v2.30.0-pre7 3ee801c

service.go RawHistoryFolder

1// Package health implements the health check use case for routes.
2package health
3
4import (
5	"context"
6	"fmt"
7	"sync"
8
9	"github.com/bnema/zerowrap"
10
11	"github.com/bnema/gordon/internal/boundaries/in"
12	"github.com/bnema/gordon/internal/domain"
13)
14
15// maxConcurrentProbes limits the number of concurrent health probes to prevent resource exhaustion.
16const maxConcurrentProbes = 10
17
18// Service implements the HealthService interface.
19type Service struct {
20	configSvc    in.ConfigService
21	containerSvc in.ContainerService
22	prober       in.HTTPProber
23	log          zerowrap.Logger
24}
25
26// NewService creates a new health service.
27func NewService(
28	configSvc in.ConfigService,
29	containerSvc in.ContainerService,
30	prober in.HTTPProber,
31	log zerowrap.Logger,
32) *Service {
33	return &Service{
34		configSvc:    configSvc,
35		containerSvc: containerSvc,
36		prober:       prober,
37		log:          log,
38	}
39}
40
41// CheckRoute performs a health check on a single route.
42func (s *Service) CheckRoute(ctx context.Context, route domain.Route) *domain.RouteHealth {
43	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
44		zerowrap.FieldLayer:   "usecase",
45		zerowrap.FieldUseCase: "CheckRoute",
46		"domain":              route.Domain,
47	})
48	log := zerowrap.FromCtx(ctx)
49
50	health := &domain.RouteHealth{
51		Domain:          route.Domain,
52		ContainerStatus: "unknown",
53	}
54
55	// Check container status
56	container, exists := s.containerSvc.Get(ctx, route.Domain)
57	if !exists || container == nil {
58		health.ContainerStatus = "not found"
59		health.Error = "container not found"
60		log.Debug().Msg("container not found")
61		return health
62	}
63
64	health.ContainerStatus = container.Status
65
66	// Only probe HTTP if container is running
67	if container.Status != string(domain.ContainerStatusRunning) {
68		health.Error = fmt.Sprintf("container is %s", container.Status)
69		log.Debug().Str("status", container.Status).Msg("container not running, skipping HTTP probe")
70		return health
71	}
72
73	// Probe HTTP endpoint
74	url := fmt.Sprintf("https://%s/", route.Domain)
75	statusCode, responseTime, err := s.prober.Probe(ctx, url)
76	if err != nil {
77		health.Error = err.Error()
78		log.Debug().Err(err).Str("url", url).Msg("HTTP probe failed")
79		return health
80	}
81
82	health.HTTPStatus = statusCode
83	health.ResponseTimeMs = responseTime
84	health.Healthy = statusCode >= 200 && statusCode < 400
85
86	log.Debug().
87		Int("http_status", statusCode).
88		Int64("response_time_ms", responseTime).
89		Bool("healthy", health.Healthy).
90		Msg("health check complete")
91
92	return health
93}
94
95// CheckAllRoutes performs health checks on all configured routes.
96func (s *Service) CheckAllRoutes(ctx context.Context) map[string]*domain.RouteHealth {
97	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
98		zerowrap.FieldLayer:   "usecase",
99		zerowrap.FieldUseCase: "CheckAllRoutes",
100	})
101	log := zerowrap.FromCtx(ctx)
102
103	routes := s.configSvc.GetRoutes(ctx)
104	results := make(map[string]*domain.RouteHealth, len(routes))
105
106	if len(routes) == 0 {
107		log.Debug().Msg("no routes configured")
108		return results
109	}
110
111	// Check routes concurrently with semaphore to limit resource usage
112	var mu sync.Mutex
113	var wg sync.WaitGroup
114	sem := make(chan struct{}, maxConcurrentProbes)
115
116	for _, route := range routes {
117		wg.Add(1)
118		go func(r domain.Route) {
119			defer wg.Done()
120			sem <- struct{}{}        // Acquire semaphore
121			defer func() { <-sem }() // Release semaphore
122
123			health := s.CheckRoute(ctx, r)
124			mu.Lock()
125			results[r.Domain] = health
126			mu.Unlock()
127		}(route)
128	}
129
130	wg.Wait()
131
132	log.Debug().Int("routes_checked", len(results)).Msg("all health checks complete")
133	return results
134}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close