···1+package server
2+3+import "errors"
4+5+// Common errors used across the server package
6+var (
7+ ErrMissingToken = errors.New("missing token")
8+ ErrMonitorNotFound = errors.New("monitor not found")
9+ ErrPrivateLocationNotFound = errors.New("private location not found")
10+)
···1package server_test
23import (
4+ "context"
5 "database/sql"
6 "testing"
78+ "connectrpc.com/connect"
9 "github.com/openstatushq/openstatus/apps/private-location/internal/server"
10 private_locationv1 "github.com/openstatushq/openstatus/apps/private-location/proto/private_location/v1"
11)
···57 t.Errorf("expected Comparator to be STRING_COMPARATOR_CONTAINS, got %v", got.Comparator)
58 }
59}
60+61+func TestParseAssertions_InvalidJSON(t *testing.T) {
62+ assertions := sql.NullString{
63+ String: "not valid json",
64+ Valid: true,
65+ }
66+67+ statusAssertions, headerAssertions, bodyAssertions := server.ParseAssertions(assertions)
68+69+ if len(statusAssertions) != 0 || len(headerAssertions) != 0 || len(bodyAssertions) != 0 {
70+ t.Errorf("expected empty assertions for invalid JSON, got status=%d, header=%d, body=%d",
71+ len(statusAssertions), len(headerAssertions), len(bodyAssertions))
72+ }
73+}
74+75+func TestParseAssertions_NullString(t *testing.T) {
76+ assertions := sql.NullString{
77+ String: "",
78+ Valid: false,
79+ }
80+81+ statusAssertions, headerAssertions, bodyAssertions := server.ParseAssertions(assertions)
82+83+ if len(statusAssertions) != 0 || len(headerAssertions) != 0 || len(bodyAssertions) != 0 {
84+ t.Errorf("expected empty assertions for null string, got status=%d, header=%d, body=%d",
85+ len(statusAssertions), len(headerAssertions), len(bodyAssertions))
86+ }
87+}
88+89+func TestParseAssertions_HeaderAssertion(t *testing.T) {
90+ input := `[{"version":"v1","type":"header","compare":"eq","key":"Content-Type","target":"application/json"}]`
91+ assertions := sql.NullString{
92+ String: input,
93+ Valid: true,
94+ }
95+96+ _, headerAssertions, _ := server.ParseAssertions(assertions)
97+98+ if len(headerAssertions) != 1 {
99+ t.Fatalf("expected 1 header assertion, got %d", len(headerAssertions))
100+ }
101+102+ got := headerAssertions[0]
103+ if got.Key != "Content-Type" {
104+ t.Errorf("expected Key to be 'Content-Type', got '%s'", got.Key)
105+ }
106+ if got.Target != "application/json" {
107+ t.Errorf("expected Target to be 'application/json', got '%s'", got.Target)
108+ }
109+ if got.Comparator != private_locationv1.StringComparator_STRING_COMPARATOR_EQUAL {
110+ t.Errorf("expected Comparator to be STRING_COMPARATOR_EQUAL, got %v", got.Comparator)
111+ }
112+}
113+114+func TestParseAssertions_MultipleAssertions(t *testing.T) {
115+ input := `[
116+ {"version":"v1","type":"status","compare":"eq","target":200},
117+ {"version":"v1","type":"header","compare":"contains","key":"X-Request-Id","target":"req-"},
118+ {"version":"v1","type":"textBody","compare":"notContains","target":"error"}
119+ ]`
120+ assertions := sql.NullString{
121+ String: input,
122+ Valid: true,
123+ }
124+125+ statusAssertions, headerAssertions, bodyAssertions := server.ParseAssertions(assertions)
126+127+ if len(statusAssertions) != 1 {
128+ t.Errorf("expected 1 status assertion, got %d", len(statusAssertions))
129+ }
130+ if len(headerAssertions) != 1 {
131+ t.Errorf("expected 1 header assertion, got %d", len(headerAssertions))
132+ }
133+ if len(bodyAssertions) != 1 {
134+ t.Errorf("expected 1 body assertion, got %d", len(bodyAssertions))
135+ }
136+}
137+138+func TestMonitors_Unauthenticated(t *testing.T) {
139+ h := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))
140+141+ req := connect.NewRequest(&private_locationv1.MonitorsRequest{})
142+ // No token header
143+ resp, err := h.Monitors(context.Background(), req)
144+ if err == nil {
145+ t.Fatalf("expected error for missing token, got nil")
146+ }
147+ if connect.CodeOf(err) != connect.CodeUnauthenticated {
148+ t.Errorf("expected unauthenticated code, got %v", connect.CodeOf(err))
149+ }
150+ if resp != nil {
151+ t.Errorf("expected nil response, got %v", resp)
152+ }
153+}
154+155+func TestMonitors_InvalidToken(t *testing.T) {
156+ h := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))
157+158+ req := connect.NewRequest(&private_locationv1.MonitorsRequest{})
159+ req.Header().Set("openstatus-token", "invalid-token")
160+161+ resp, err := h.Monitors(context.Background(), req)
162+ if err != nil {
163+ t.Fatalf("expected no error for invalid token (just empty results), got %v", err)
164+ }
165+ if resp == nil {
166+ t.Fatalf("expected non-nil response")
167+ }
168+ if len(resp.Msg.HttpMonitors) != 0 {
169+ t.Errorf("expected 0 HTTP monitors for invalid token, got %d", len(resp.Msg.HttpMonitors))
170+ }
171+ if len(resp.Msg.TcpMonitors) != 0 {
172+ t.Errorf("expected 0 TCP monitors for invalid token, got %d", len(resp.Msg.TcpMonitors))
173+ }
174+ if len(resp.Msg.DnsMonitors) != 0 {
175+ t.Errorf("expected 0 DNS monitors for invalid token, got %d", len(resp.Msg.DnsMonitors))
176+ }
177+}
178+179+func TestMonitors_ReturnsHTTPTCPAndDNSMonitors(t *testing.T) {
180+ h := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))
181+182+ req := connect.NewRequest(&private_locationv1.MonitorsRequest{})
183+ req.Header().Set("openstatus-token", "my-secret-key")
184+185+ resp, err := h.Monitors(context.Background(), req)
186+ if err != nil {
187+ t.Fatalf("expected no error, got %v", err)
188+ }
189+ if resp == nil {
190+ t.Fatalf("expected non-nil response")
191+ }
192+193+ // Should have HTTP monitor (monitor ID 5)
194+ if len(resp.Msg.HttpMonitors) != 1 {
195+ t.Errorf("expected 1 HTTP monitor, got %d", len(resp.Msg.HttpMonitors))
196+ }
197+198+ // Should have TCP monitor (monitor ID 6)
199+ if len(resp.Msg.TcpMonitors) != 1 {
200+ t.Errorf("expected 1 TCP monitor, got %d", len(resp.Msg.TcpMonitors))
201+ }
202+203+ // Should have DNS monitor (monitor ID 7)
204+ if len(resp.Msg.DnsMonitors) != 1 {
205+ t.Errorf("expected 1 DNS monitor, got %d", len(resp.Msg.DnsMonitors))
206+ }
207+}
208+209+func TestMonitors_HTTPMonitorFields(t *testing.T) {
210+ h := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))
211+212+ req := connect.NewRequest(&private_locationv1.MonitorsRequest{})
213+ req.Header().Set("openstatus-token", "my-secret-key")
214+215+ resp, err := h.Monitors(context.Background(), req)
216+ if err != nil {
217+ t.Fatalf("expected no error, got %v", err)
218+ }
219+ if len(resp.Msg.HttpMonitors) != 1 {
220+ t.Fatalf("expected 1 HTTP monitor, got %d", len(resp.Msg.HttpMonitors))
221+ }
222+223+ httpMonitor := resp.Msg.HttpMonitors[0]
224+ if httpMonitor.Id != "5" {
225+ t.Errorf("expected ID '5', got '%s'", httpMonitor.Id)
226+ }
227+ if httpMonitor.Url != "https://openstat.us" {
228+ t.Errorf("expected URL 'https://openstat.us', got '%s'", httpMonitor.Url)
229+ }
230+ if httpMonitor.Periodicity != "10m" {
231+ t.Errorf("expected Periodicity '10m', got '%s'", httpMonitor.Periodicity)
232+ }
233+ if httpMonitor.Method != "GET" {
234+ t.Errorf("expected Method 'GET', got '%s'", httpMonitor.Method)
235+ }
236+ if httpMonitor.Timeout != 45000 {
237+ t.Errorf("expected Timeout 45000, got %d", httpMonitor.Timeout)
238+ }
239+}
240+241+func TestMonitors_TCPMonitorFields(t *testing.T) {
242+ h := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))
243+244+ req := connect.NewRequest(&private_locationv1.MonitorsRequest{})
245+ req.Header().Set("openstatus-token", "my-secret-key")
246+247+ resp, err := h.Monitors(context.Background(), req)
248+ if err != nil {
249+ t.Fatalf("expected no error, got %v", err)
250+ }
251+ if len(resp.Msg.TcpMonitors) != 1 {
252+ t.Fatalf("expected 1 TCP monitor, got %d", len(resp.Msg.TcpMonitors))
253+ }
254+255+ tcpMonitor := resp.Msg.TcpMonitors[0]
256+ if tcpMonitor.Id != "6" {
257+ t.Errorf("expected ID '6', got '%s'", tcpMonitor.Id)
258+ }
259+ if tcpMonitor.Uri != "tcp://db.example.com:5432" {
260+ t.Errorf("expected URI 'tcp://db.example.com:5432', got '%s'", tcpMonitor.Uri)
261+ }
262+ if tcpMonitor.Periodicity != "5m" {
263+ t.Errorf("expected Periodicity '5m', got '%s'", tcpMonitor.Periodicity)
264+ }
265+ if tcpMonitor.Timeout != 30000 {
266+ t.Errorf("expected Timeout 30000, got %d", tcpMonitor.Timeout)
267+ }
268+ if tcpMonitor.Retry != 2 {
269+ t.Errorf("expected Retry 2, got %d", tcpMonitor.Retry)
270+ }
271+ if tcpMonitor.DegradedAt == nil || *tcpMonitor.DegradedAt != 5000 {
272+ t.Errorf("expected DegradedAt 5000, got %v", tcpMonitor.DegradedAt)
273+ }
274+}
275+276+func TestParseRecordAssertions_DnsRecordContains(t *testing.T) {
277+ input := `[{"version":"v1","type":"dnsRecord","key":"A","compare":"contains","target":"76.76.21.21"}]`
278+ assertions := sql.NullString{
279+ String: input,
280+ Valid: true,
281+ }
282+283+ recordAssertions := server.ParseRecordAssertions(assertions)
284+285+ if len(recordAssertions) != 1 {
286+ t.Fatalf("expected 1 record assertion, got %d", len(recordAssertions))
287+ }
288+289+ got := recordAssertions[0]
290+ if got.Record != "A" {
291+ t.Errorf("expected Record to be 'A', got '%s'", got.Record)
292+ }
293+ if got.Target != "76.76.21.21" {
294+ t.Errorf("expected Target to be '76.76.21.21', got '%s'", got.Target)
295+ }
296+ if got.Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_CONTAINS {
297+ t.Errorf("expected Comparator to be RECORD_COMPARATOR_CONTAINS, got %v", got.Comparator)
298+ }
299+}
300+301+func TestParseRecordAssertions_DnsRecordEquals(t *testing.T) {
302+ input := `[{"version":"v1","type":"dnsRecord","key":"CNAME","compare":"eq","target":"openstatus.dev"}]`
303+ assertions := sql.NullString{
304+ String: input,
305+ Valid: true,
306+ }
307+308+ recordAssertions := server.ParseRecordAssertions(assertions)
309+310+ if len(recordAssertions) != 1 {
311+ t.Fatalf("expected 1 record assertion, got %d", len(recordAssertions))
312+ }
313+314+ got := recordAssertions[0]
315+ if got.Record != "CNAME" {
316+ t.Errorf("expected Record to be 'CNAME', got '%s'", got.Record)
317+ }
318+ if got.Target != "openstatus.dev" {
319+ t.Errorf("expected Target to be 'openstatus.dev', got '%s'", got.Target)
320+ }
321+ if got.Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_EQUAL {
322+ t.Errorf("expected Comparator to be RECORD_COMPARATOR_EQUAL, got %v", got.Comparator)
323+ }
324+}
325+326+func TestParseRecordAssertions_MultipleRecordTypes(t *testing.T) {
327+ input := `[
328+ {"version":"v1","type":"dnsRecord","key":"A","compare":"eq","target":"192.168.1.1"},
329+ {"version":"v1","type":"dnsRecord","key":"AAAA","compare":"not_eq","target":"::1"},
330+ {"version":"v1","type":"dnsRecord","key":"MX","compare":"not_contains","target":"spam"}
331+ ]`
332+ assertions := sql.NullString{
333+ String: input,
334+ Valid: true,
335+ }
336+337+ recordAssertions := server.ParseRecordAssertions(assertions)
338+339+ if len(recordAssertions) != 3 {
340+ t.Fatalf("expected 3 record assertions, got %d", len(recordAssertions))
341+ }
342+343+ // Check A record
344+ if recordAssertions[0].Record != "A" {
345+ t.Errorf("expected first Record to be 'A', got '%s'", recordAssertions[0].Record)
346+ }
347+ if recordAssertions[0].Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_EQUAL {
348+ t.Errorf("expected first Comparator to be RECORD_COMPARATOR_EQUAL, got %v", recordAssertions[0].Comparator)
349+ }
350+351+ // Check AAAA record
352+ if recordAssertions[1].Record != "AAAA" {
353+ t.Errorf("expected second Record to be 'AAAA', got '%s'", recordAssertions[1].Record)
354+ }
355+ if recordAssertions[1].Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_NOT_EQUAL {
356+ t.Errorf("expected second Comparator to be RECORD_COMPARATOR_NOT_EQUAL, got %v", recordAssertions[1].Comparator)
357+ }
358+359+ // Check MX record
360+ if recordAssertions[2].Record != "MX" {
361+ t.Errorf("expected third Record to be 'MX', got '%s'", recordAssertions[2].Record)
362+ }
363+ if recordAssertions[2].Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_NOT_CONTAINS {
364+ t.Errorf("expected third Comparator to be RECORD_COMPARATOR_NOT_CONTAINS, got %v", recordAssertions[2].Comparator)
365+ }
366+}
367+368+func TestParseRecordAssertions_InvalidJSON(t *testing.T) {
369+ assertions := sql.NullString{
370+ String: "not valid json",
371+ Valid: true,
372+ }
373+374+ recordAssertions := server.ParseRecordAssertions(assertions)
375+376+ if len(recordAssertions) != 0 {
377+ t.Errorf("expected empty assertions for invalid JSON, got %d", len(recordAssertions))
378+ }
379+}
380+381+func TestParseRecordAssertions_NullString(t *testing.T) {
382+ assertions := sql.NullString{
383+ String: "",
384+ Valid: false,
385+ }
386+387+ recordAssertions := server.ParseRecordAssertions(assertions)
388+389+ if recordAssertions != nil {
390+ t.Errorf("expected nil for null string, got %v", recordAssertions)
391+ }
392+}
393+394+func TestParseRecordAssertions_MixedAssertionTypes(t *testing.T) {
395+ // Test that only dnsRecord assertions are parsed, not other types
396+ input := `[
397+ {"version":"v1","type":"status","compare":"eq","target":200},
398+ {"version":"v1","type":"dnsRecord","key":"A","compare":"contains","target":"192.168.1.1"},
399+ {"version":"v1","type":"header","compare":"eq","key":"Content-Type","target":"application/json"}
400+ ]`
401+ assertions := sql.NullString{
402+ String: input,
403+ Valid: true,
404+ }
405+406+ recordAssertions := server.ParseRecordAssertions(assertions)
407+408+ if len(recordAssertions) != 1 {
409+ t.Fatalf("expected 1 record assertion (only dnsRecord), got %d", len(recordAssertions))
410+ }
411+412+ if recordAssertions[0].Record != "A" {
413+ t.Errorf("expected Record to be 'A', got '%s'", recordAssertions[0].Record)
414+ }
415+}
416+417+func TestMonitors_DNSMonitorFields(t *testing.T) {
418+ h := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))
419+420+ req := connect.NewRequest(&private_locationv1.MonitorsRequest{})
421+ req.Header().Set("openstatus-token", "my-secret-key")
422+423+ resp, err := h.Monitors(context.Background(), req)
424+ if err != nil {
425+ t.Fatalf("expected no error, got %v", err)
426+ }
427+ if len(resp.Msg.DnsMonitors) != 1 {
428+ t.Fatalf("expected 1 DNS monitor, got %d", len(resp.Msg.DnsMonitors))
429+ }
430+431+ dnsMonitor := resp.Msg.DnsMonitors[0]
432+ if dnsMonitor.Id != "7" {
433+ t.Errorf("expected ID '7', got '%s'", dnsMonitor.Id)
434+ }
435+ if dnsMonitor.Uri != "openstatus.dev" {
436+ t.Errorf("expected URI 'openstatus.dev', got '%s'", dnsMonitor.Uri)
437+ }
438+ if dnsMonitor.Periodicity != "5m" {
439+ t.Errorf("expected Periodicity '5m', got '%s'", dnsMonitor.Periodicity)
440+ }
441+ if dnsMonitor.Timeout != 30000 {
442+ t.Errorf("expected Timeout 30000, got %d", dnsMonitor.Timeout)
443+ }
444+ if dnsMonitor.Retry != 2 {
445+ t.Errorf("expected Retry 2, got %d", dnsMonitor.Retry)
446+ }
447+ if dnsMonitor.DegradedAt == nil || *dnsMonitor.DegradedAt != 3000 {
448+ t.Errorf("expected DegradedAt 3000, got %v", dnsMonitor.DegradedAt)
449+ }
450+}
451+452+func TestParseRecordAssertions_EmptyArray(t *testing.T) {
453+ assertions := sql.NullString{
454+ String: "[]",
455+ Valid: true,
456+ }
457+458+ recordAssertions := server.ParseRecordAssertions(assertions)
459+460+ if len(recordAssertions) != 0 {
461+ t.Errorf("expected empty slice for empty JSON array, got %d", len(recordAssertions))
462+ }
463+}
464+465+func TestParseRecordAssertions_UnknownComparator(t *testing.T) {
466+ input := `[{"version":"v1","type":"dnsRecord","key":"A","compare":"unknown_comparator","target":"192.168.1.1"}]`
467+ assertions := sql.NullString{
468+ String: input,
469+ Valid: true,
470+ }
471+472+ recordAssertions := server.ParseRecordAssertions(assertions)
473+474+ if len(recordAssertions) != 1 {
475+ t.Fatalf("expected 1 record assertion, got %d", len(recordAssertions))
476+ }
477+478+ got := recordAssertions[0]
479+ if got.Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_UNSPECIFIED {
480+ t.Errorf("expected Comparator to be RECORD_COMPARATOR_UNSPECIFIED for unknown comparator, got %v", got.Comparator)
481+ }
482+}
483+484+func TestParseRecordAssertions_UnknownRecordType(t *testing.T) {
485+ input := `[{"version":"v1","type":"dnsRecord","key":"INVALID_RECORD_TYPE","compare":"eq","target":"test"}]`
486+ assertions := sql.NullString{
487+ String: input,
488+ Valid: true,
489+ }
490+491+ recordAssertions := server.ParseRecordAssertions(assertions)
492+493+ if len(recordAssertions) != 1 {
494+ t.Fatalf("expected 1 record assertion, got %d", len(recordAssertions))
495+ }
496+497+ got := recordAssertions[0]
498+ // The record type is passed through as-is, even if invalid
499+ if got.Record != "INVALID_RECORD_TYPE" {
500+ t.Errorf("expected Record to be 'INVALID_RECORD_TYPE', got '%s'", got.Record)
501+ }
502+}
503+504+func TestParseRecordAssertions_MissingRequiredFields(t *testing.T) {
505+ // Missing "key" field
506+ inputMissingKey := `[{"version":"v1","type":"dnsRecord","compare":"eq","target":"test"}]`
507+ assertions := sql.NullString{
508+ String: inputMissingKey,
509+ Valid: true,
510+ }
511+512+ recordAssertions := server.ParseRecordAssertions(assertions)
513+514+ if len(recordAssertions) != 1 {
515+ t.Fatalf("expected 1 record assertion, got %d", len(recordAssertions))
516+ }
517+518+ // Missing key results in empty string
519+ if recordAssertions[0].Record != "" {
520+ t.Errorf("expected empty Record for missing key, got '%s'", recordAssertions[0].Record)
521+ }
522+523+ // Missing "compare" field
524+ inputMissingCompare := `[{"version":"v1","type":"dnsRecord","key":"A","target":"test"}]`
525+ assertions2 := sql.NullString{
526+ String: inputMissingCompare,
527+ Valid: true,
528+ }
529+530+ recordAssertions2 := server.ParseRecordAssertions(assertions2)
531+532+ if len(recordAssertions2) != 1 {
533+ t.Fatalf("expected 1 record assertion, got %d", len(recordAssertions2))
534+ }
535+536+ // Missing compare results in unspecified comparator
537+ if recordAssertions2[0].Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_UNSPECIFIED {
538+ t.Errorf("expected RECORD_COMPARATOR_UNSPECIFIED for missing compare, got %v", recordAssertions2[0].Comparator)
539+ }
540+541+ // Missing "target" field
542+ inputMissingTarget := `[{"version":"v1","type":"dnsRecord","key":"A","compare":"eq"}]`
543+ assertions3 := sql.NullString{
544+ String: inputMissingTarget,
545+ Valid: true,
546+ }
547+548+ recordAssertions3 := server.ParseRecordAssertions(assertions3)
549+550+ if len(recordAssertions3) != 1 {
551+ t.Fatalf("expected 1 record assertion, got %d", len(recordAssertions3))
552+ }
553+554+ // Missing target results in empty string
555+ if recordAssertions3[0].Target != "" {
556+ t.Errorf("expected empty Target for missing target, got '%s'", recordAssertions3[0].Target)
557+ }
558+}
559+560+func TestMonitors_DNSMonitorWithRecordAssertions(t *testing.T) {
561+ h := server.NewPrivateLocationServer(testDB(), getTBClient(context.Background()))
562+563+ req := connect.NewRequest(&private_locationv1.MonitorsRequest{})
564+ req.Header().Set("openstatus-token", "my-secret-key")
565+566+ resp, err := h.Monitors(context.Background(), req)
567+ if err != nil {
568+ t.Fatalf("expected no error, got %v", err)
569+ }
570+ if len(resp.Msg.DnsMonitors) != 1 {
571+ t.Fatalf("expected 1 DNS monitor, got %d", len(resp.Msg.DnsMonitors))
572+ }
573+574+ dnsMonitor := resp.Msg.DnsMonitors[0]
575+ if len(dnsMonitor.RecordAssertions) != 1 {
576+ t.Fatalf("expected 1 record assertion, got %d", len(dnsMonitor.RecordAssertions))
577+ }
578+579+ assertion := dnsMonitor.RecordAssertions[0]
580+ if assertion.Record != "A" {
581+ t.Errorf("expected Record 'A', got '%s'", assertion.Record)
582+ }
583+ if assertion.Target != "76.76.21.21" {
584+ t.Errorf("expected Target '76.76.21.21', got '%s'", assertion.Target)
585+ }
586+ if assertion.Comparator != private_locationv1.RecordComparator_RECORD_COMPARATOR_CONTAINS {
587+ t.Errorf("expected Comparator RECORD_COMPARATOR_CONTAINS, got %v", assertion.Comparator)
588+ }
589+}
+10-2
apps/private-location/internal/server/routes.go
···161162// healthHandler responds with the health status of the server.
163func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
001640000000165 render.JSON(w, r, map[string]any{
166- "status": "ok",
167 })
168- render.Status(r, http.StatusOK)
169}
···161162// healthHandler responds with the health status of the server.
163func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
164+ status := "ok"
165+ httpStatus := http.StatusOK
166167+ // Check database connection
168+ if err := s.db.PingContext(r.Context()); err != nil {
169+ status = "degraded"
170+ httpStatus = http.StatusServiceUnavailable
171+ }
172+173+ render.Status(r, httpStatus)
174 render.JSON(w, r, map[string]any{
175+ "status": status,
176 })
0177}
+10-1
apps/private-location/internal/server/server.go
···3132// NewServer returns an HTTP server and a cleanup function to shutdown the log provider.
33func NewServer() (*http.Server, func(context.Context)) {
34- port, _ := strconv.Atoi(os.Getenv("PORT"))
000000003536 logger, logProvider := setupLogger()
37···56 if logProvider != nil {
57 logProvider.Shutdown(ctx)
58 }
059 }
6061 return server, cleanup
···3132// NewServer returns an HTTP server and a cleanup function to shutdown the log provider.
33func NewServer() (*http.Server, func(context.Context)) {
34+ portStr := os.Getenv("PORT")
35+ if portStr == "" {
36+ portStr = "8080"
37+ }
38+ port, err := strconv.Atoi(portStr)
39+ if err != nil {
40+ fmt.Fprintf(os.Stderr, "invalid PORT value %q: %v\n", portStr, err)
41+ os.Exit(1)
42+ }
4344 logger, logProvider := setupLogger()
45···64 if logProvider != nil {
65 logProvider.Shutdown(ctx)
66 }
67+ database.Close()
68 }
6970 return server, cleanup
···12 "github.com/rs/zerolog/log"
13)
14000000015func getBaseURL() string {
16 // Use local Tinybird container if available (Docker/self-hosted)
17 // https://www.tinybird.co/docs/api-reference
···12 "github.com/rs/zerolog/log"
13)
1415+// Datasource names for Tinybird events
16+const (
17+ DatasourceHTTP = "ping_response__v8"
18+ DatasourceTCP = "tcp_response__v0"
19+ DatasourceDNS = "tcp_dns__v0"
20+)
21+22func getBaseURL() string {
23 // Use local Tinybird container if available (Docker/self-hosted)
24 // https://www.tinybird.co/docs/api-reference