Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1a497f9
Add TOTP two-factor authentication
techulus-agent Jul 9, 2026
49bf423
Replace service request logs with metrics
techulus-agent Jul 9, 2026
76b442b
Add QR code to 2FA setup
techulus-agent Jul 9, 2026
f88b2c4
Reduce service metric label cardinality
techulus-agent Jul 9, 2026
1ee14b0
Add agent process resource metrics
techulus-agent Jul 9, 2026
91dfe6a
Merge pull request #116 from techulus/feat-auth-2fa
arjunkomath Jul 9, 2026
9ea0ed0
Merge pull request #117 from techulus/feat/service-level-metrics
arjunkomath Jul 9, 2026
779abb2
Fix service metrics window alignment
arjunkomath Jul 9, 2026
c2721b4
Always show service metrics history
arjunkomath Jul 9, 2026
16ad330
Scope service metric summary to selected tab
arjunkomath Jul 9, 2026
67f82f5
Group service request metrics by status family
arjunkomath Jul 9, 2026
121c0d8
Merge pull request #119 from techulus/fix/service-metrics-window-alig…
arjunkomath Jul 9, 2026
7a0934e
Require 2FA before deleting services
techulus-agent Jul 9, 2026
7066f15
Inline service delete 2FA check
techulus-agent Jul 9, 2026
5bd3fcf
Simplify service delete confirmation
techulus-agent Jul 9, 2026
69ae8de
Use auth code only for service delete confirmation
techulus-agent Jul 9, 2026
e51e1bf
Use shadcn OTP input for service delete code
techulus-agent Jul 9, 2026
fd6ddd8
Use OTP input for auth codes
techulus-agent Jul 9, 2026
13b97c6
Throttle service delete TOTP attempts
techulus-agent Jul 9, 2026
b6f6125
Clean up
arjunkomath Jul 9, 2026
a38ac10
Clean up
arjunkomath Jul 9, 2026
b5c2192
Merge pull request #120 from techulus/feat/require-2fa-service-delete
arjunkomath Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/node_modules
/.pnp
.pnp.js
.pnpm-store/

# testing
/coverage
Expand Down
3 changes: 3 additions & 0 deletions agent/internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ func NewAgent(

type MetricsSender interface {
SendSystemStats(stats *health.SystemStats, collectedAt time.Time) error
SendAgentStats(stats *health.AgentProcessStats, collectedAt time.Time) error
SendContainerStats(stats []container.ResourceStats, collectedAt time.Time) error
SendPrometheusMetrics(data []byte, extraLabels map[string]string) error
}

func (a *Agent) GetState() AgentState {
Expand Down
10 changes: 8 additions & 2 deletions agent/internal/agent/drift.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,13 +575,19 @@ func (a *Agent) updateTraefik() error {
}

needsRestart := false
metricsRestart, err := traefik.EnsureMetricsConfig()
if err != nil {
return fmt.Errorf("failed to ensure Traefik metrics config: %w", err)
}
needsRestart = metricsRestart

if len(tcpPorts) > 0 || len(udpPorts) > 0 {
log.Printf("[reconcile] ensuring L4 entry points: %d TCP, %d UDP", len(tcpPorts), len(udpPorts))
var err error
needsRestart, err = traefik.EnsureEntryPoints(tcpPorts, udpPorts)
entryPointsRestart, err := traefik.EnsureEntryPoints(tcpPorts, udpPorts)
if err != nil {
return fmt.Errorf("failed to ensure entry points: %w", err)
}
needsRestart = needsRestart || entryPointsRestart
}

log.Printf("[reconcile] updating Traefik routes (HTTP: %d, TCP: %d, UDP: %d)", len(expectedHttpRoutes), len(tcpRoutes), len(udpRoutes))
Expand Down
14 changes: 14 additions & 0 deletions agent/internal/agent/reporting.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ func (a *Agent) BuildStatusReport(includeResources bool) *agenthttp.StatusReport
if err := a.MetricsSender.SendSystemStats(systemStats, collectedAt); err != nil {
log.Printf("[metrics] failed to send system stats: %v", err)
}
agentStats, err := health.CollectAgentProcessStats()
if err != nil {
log.Printf("[metrics] failed to collect agent stats: %v", err)
} else if err := a.MetricsSender.SendAgentStats(agentStats, collectedAt); err != nil {
log.Printf("[metrics] failed to send agent stats: %v", err)
}
containerStats, err := container.CollectResourceStats()
if err != nil {
log.Printf("[metrics] failed to collect container stats: %v", err)
return
}
if err := a.MetricsSender.SendContainerStats(containerStats, collectedAt); err != nil {
log.Printf("[metrics] failed to send container stats: %v", err)
}
}()
}
report.NetworkHealth = health.CollectNetworkHealth("wg0")
Expand Down
65 changes: 65 additions & 0 deletions agent/internal/agent/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ package agent

import (
"context"
"fmt"
"io"
"log"
"net/http"
"time"

"techulus/cloud-agent/internal/container"
"techulus/cloud-agent/internal/serverless"
)

const (
traefikMetricsURL = "http://127.0.0.1:9100/metrics"
traefikMetricsInterval = 15 * time.Second
traefikMetricsMaxBytes = 8 * 1024 * 1024
)

func (a *Agent) Run(ctx context.Context) {
if a.Config.RegistryURL != "" && a.Config.RegistryUsername != "" && a.Config.RegistryPassword != "" {
if err := container.Login(a.Config.RegistryURL, a.Config.RegistryUsername, a.Config.RegistryPassword, a.Config.RegistryInsecure); err != nil {
Expand All @@ -33,6 +42,10 @@ func (a *Agent) Run(ctx context.Context) {
a.TraefikLogCollector.Start()
}

if a.IsProxy && a.MetricsSender != nil {
go a.TraefikMetricsLoop(ctx)
}

if a.IsProxy {
gateway := serverless.NewGateway(a)
if err := gateway.Start(ctx); err != nil {
Expand Down Expand Up @@ -81,6 +94,58 @@ func (a *Agent) Run(ctx context.Context) {
}
}

func (a *Agent) TraefikMetricsLoop(ctx context.Context) {
ticker := time.NewTicker(traefikMetricsInterval)
defer ticker.Stop()

if err := a.ForwardTraefikMetrics(ctx); err != nil {
log.Printf("[traefik-metrics] initial scrape failed: %v", err)
}

for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := a.ForwardTraefikMetrics(ctx); err != nil {
log.Printf("[traefik-metrics] scrape failed: %v", err)
}
}
}
}

func (a *Agent) ForwardTraefikMetrics(ctx context.Context) error {
if a.MetricsSender == nil {
return nil
}

request, err := http.NewRequestWithContext(ctx, http.MethodGet, traefikMetricsURL, nil)
if err != nil {
return err
}

client := &http.Client{Timeout: 10 * time.Second}
response, err := client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()

if response.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status %d", response.StatusCode)
}

body, err := io.ReadAll(io.LimitReader(response.Body, traefikMetricsMaxBytes))
if err != nil {
return err
}

return a.MetricsSender.SendPrometheusMetrics(body, map[string]string{
"job": "traefik",
"server_id": a.Config.ServerID,
})
}

func (a *Agent) StatusReportLoop(ctx context.Context) {
a.reportStatus("startup")

Expand Down
Loading
Loading