Skip to content

Commit e90079b

Browse files
start logging in checker (#1755)
1 parent 1a082db commit e90079b

File tree

4 files changed

+299
-79
lines changed

4 files changed

+299
-79
lines changed

apps/checker/cmd/server/main.go

Lines changed: 174 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,157 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"log/slog"
8+
"math/rand/v2"
79
"net/http"
810
"os"
911
"os/signal"
1012
"syscall"
1113
"time"
1214

1315
"github.com/gin-gonic/gin"
16+
"github.com/google/uuid"
1417
"github.com/openstatushq/openstatus/apps/checker/handlers"
1518

1619
"github.com/openstatushq/openstatus/apps/checker/pkg/logger"
1720
"github.com/openstatushq/openstatus/apps/checker/pkg/tinybird"
1821
"github.com/rs/zerolog/log"
22+
"go.opentelemetry.io/contrib/bridges/otelslog"
23+
// otelz "go.opentelemetry.io/contrib/bridges/otelzerolog"
24+
"go.opentelemetry.io/otel/log/global"
25+
"go.opentelemetry.io/otel/attribute"
26+
otlploghttp "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
27+
sdklog "go.opentelemetry.io/otel/sdk/log"
28+
"go.opentelemetry.io/otel/sdk/resource"
29+
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
1930
)
2031

32+
func shouldSample(event map[string]any) bool {
33+
statusCode, _ := event["status_code"].(int)
34+
durationMs, _ := event["duration_ms"].(int)
35+
36+
// Always capture: server errors
37+
if statusCode >= 500 {
38+
return true
39+
}
40+
41+
// Always capture: explicit errors
42+
if _, hasError := event["error"]; hasError {
43+
return true
44+
}
45+
46+
// Always capture: slow requests (above p99 - 2s threshold)
47+
if durationMs > 2000 {
48+
return true
49+
}
50+
51+
// Higher sampling for client errors (4xx) - 100%
52+
if statusCode >= 400 && statusCode < 500 {
53+
return true
54+
}
55+
56+
// Random sample successful, fast requests at 20%
57+
return rand.Float64() < 0.2
58+
}
59+
60+
// MapToAttrs converts a map[string]any to a slice of slog.Attr
61+
func MapToAttrs(m map[string]any) []slog.Attr {
62+
attrs := make([]slog.Attr, 0, len(m))
63+
for k, v := range m {
64+
attrs = append(attrs, toAttr(k, v))
65+
}
66+
return attrs
67+
}
68+
69+
func toAttr(key string, value any) slog.Attr {
70+
switch v := value.(type) {
71+
case string:
72+
return slog.String(key, v)
73+
case int:
74+
return slog.Int(key, v)
75+
case int64:
76+
return slog.Int64(key, v)
77+
case float64:
78+
return slog.Float64(key, v)
79+
case bool:
80+
return slog.Bool(key, v)
81+
case time.Time:
82+
return slog.Time(key, v)
83+
case time.Duration:
84+
return slog.Duration(key, v)
85+
case map[string]any:
86+
return slog.Group(key, mapToAny(v)...)
87+
default:
88+
return slog.Any(key, v)
89+
}
90+
}
91+
92+
func mapToAny(m map[string]any) []any {
93+
args := make([]any, 0, len(m)*2)
94+
for k, v := range m {
95+
args = append(args, toAttr(k, v))
96+
}
97+
return args
98+
}
99+
100+
func Logger() gin.HandlerFunc {
101+
return func(c *gin.Context) {
102+
startTime := time.Now()
103+
104+
// Generate or get request ID
105+
requestID := c.GetHeader("X-Request-ID")
106+
if requestID == "" {
107+
requestID = uuid.New().String()
108+
}
109+
c.Set("requestId", requestID)
110+
111+
// Build wide event context at request start
112+
event := map[string]any{
113+
"timestamp": startTime.Format(time.RFC3339),
114+
"request_id": requestID,
115+
"method": c.Request.Method,
116+
"path": c.Request.URL.Path,
117+
"url": c.Request.Host + c.Request.URL.String(),
118+
"user_agent": c.GetHeader("User-Agent"),
119+
"content_type": c.GetHeader("Content-Type"),
120+
}
121+
c.Set("event", event)
122+
123+
// Process request
124+
c.Next()
125+
126+
// After request - capture response details
127+
duration := time.Since(startTime).Milliseconds()
128+
status := c.Writer.Status()
129+
130+
event["status_code"] = status
131+
event["duration_ms"] = int(duration)
132+
133+
// var requestErr error
134+
if len(c.Errors) > 0 {
135+
event["outcome"] = "error"
136+
lastErr := c.Errors.Last()
137+
event["error"] = map[string]any{
138+
"type": "GinError",
139+
"message": lastErr.Error(),
140+
}
141+
} else {
142+
event["outcome"] = "success"
143+
}
144+
145+
if shouldSample(event) {
146+
attrs := MapToAttrs(event)
147+
slog.LogAttrs(c.Request.Context(),slog.LevelInfo, "request done", attrs...)
148+
}
149+
150+
log.Debug().
151+
Int("status_code", status).
152+
Int64("duration_ms", duration).
153+
Str("request_id", requestID).
154+
Msg("Request completed")
155+
}
156+
}
157+
21158
func main() {
22159
ctx, cancel := context.WithCancel(context.Background())
23160
defer cancel()
@@ -34,9 +171,10 @@ func main() {
34171
var region string
35172
cronSecret := env("CRON_SECRET", "")
36173
tinyBirdToken := env("TINYBIRD_TOKEN", "")
37-
logLevel := env("LOG_LEVEL", "warn")
174+
logLevel := env("LOG_LEVEL", "info")
38175
cloudProvider := env("CLOUD_PROVIDER", "fly")
39-
176+
axiomToken := env("AXIOM_TOKEN", "")
177+
axiomDataset := env("AXIOM_DATASET", "dev")
40178
switch cloudProvider {
41179
case "fly":
42180
region = env("FLY_REGION", env("REGION", "local"))
@@ -51,6 +189,38 @@ func main() {
51189
}
52190
logger.Configure(logLevel)
53191

192+
// Define resource with service name, version, and environment
193+
res := resource.NewWithAttributes(
194+
semconv.SchemaURL,
195+
semconv.ServiceNameKey.String("openstatus-checker"),
196+
semconv.ServiceVersionKey.String("1.0.0"),
197+
attribute.String("environment", "production"),
198+
attribute.String("cloud.provider", cloudProvider),
199+
attribute.String("cloud.region", region),
200+
)
201+
202+
// Set up OTLP log exporter for Axiom
203+
exporter, err := otlploghttp.New(ctx,
204+
otlploghttp.WithEndpointURL("https://eu-central-1.aws.edge.axiom.co/v1/logs"),
205+
otlploghttp.WithHeaders(map[string]string{
206+
"Authorization": "Bearer " + axiomToken,
207+
"X-Axiom-Dataset": axiomDataset,
208+
}),
209+
)
210+
if err != nil {
211+
log.Fatal().Err(err).Msg("failed to create OTLP exporter")
212+
}
213+
214+
// Create log provider with resource and batch processor
215+
logProvider := sdklog.NewLoggerProvider(
216+
sdklog.WithResource(res),
217+
sdklog.WithProcessor(sdklog.NewBatchProcessor(exporter)),
218+
219+
)
220+
defer logProvider.Shutdown(ctx)
221+
222+
global.SetLoggerProvider(logProvider)
223+
slog.SetDefault(otelslog.NewLogger("openstatus-checker"))
54224
httpClient := &http.Client{
55225
Timeout: 45 * time.Second,
56226
}
@@ -67,6 +237,8 @@ func main() {
67237
}
68238

69239
router := gin.New()
240+
router.Use(gin.Recovery())
241+
router.Use(Logger())
70242
router.POST("/checker", h.HTTPCheckerHandler)
71243
router.POST("/checker/http", h.HTTPCheckerHandler)
72244
router.POST("/checker/tcp", h.TCPHandler)

apps/checker/go.mod

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ require (
66
connectrpc.com/connect v1.19.1
77
github.com/gin-gonic/gin v1.9.1
88
github.com/google/uuid v1.6.0
9-
github.com/rs/zerolog v1.32.0
10-
github.com/stretchr/testify v1.10.0
11-
go.opentelemetry.io/otel v1.36.0
9+
github.com/rs/zerolog v1.34.0
10+
github.com/stretchr/testify v1.11.1
11+
go.opentelemetry.io/otel v1.39.0
1212
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.34.0
13-
go.opentelemetry.io/otel/metric v1.36.0
14-
go.opentelemetry.io/otel/sdk v1.36.0
15-
go.opentelemetry.io/otel/sdk/metric v1.36.0
16-
google.golang.org/protobuf v1.36.10
13+
go.opentelemetry.io/otel/metric v1.39.0
14+
go.opentelemetry.io/otel/sdk v1.39.0
15+
go.opentelemetry.io/otel/sdk/metric v1.39.0
16+
google.golang.org/protobuf v1.36.11
1717
)
1818

1919
//replace github.com/openstatushq/openstatus/packages/proto => ./../../packages/proto
@@ -24,14 +24,19 @@ require (
2424
github.com/cenkalti/backoff/v4 v4.3.0
2525
github.com/cenkalti/backoff/v5 v5.0.3
2626
github.com/madflojo/tasks v1.2.1
27+
github.com/vincentfree/opentelemetry/otelzerolog v0.1.1
28+
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0
29+
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.15.0
30+
go.opentelemetry.io/otel/sdk/log v0.15.0
2731
google.golang.org/api v0.247.0
2832
)
2933

3034
require (
3135
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
32-
cloud.google.com/go/compute/metadata v0.8.0 // indirect
36+
cloud.google.com/go/compute/metadata v0.9.0 // indirect
3337
cloud.google.com/go/iam v1.5.2 // indirect
3438
github.com/bytedance/sonic v1.11.3 // indirect
39+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
3540
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
3641
github.com/chenzhuoyu/iasm v0.9.1 // indirect
3742
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -47,35 +52,38 @@ require (
4752
github.com/google/s2a-go v0.1.9 // indirect
4853
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
4954
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
50-
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
55+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
5156
github.com/json-iterator/go v1.1.12 // indirect
5257
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
5358
github.com/leodido/go-urn v1.4.0 // indirect
54-
github.com/mattn/go-colorable v0.1.13 // indirect
59+
github.com/mattn/go-colorable v0.1.14 // indirect
5560
github.com/mattn/go-isatty v0.0.20 // indirect
5661
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
5762
github.com/modern-go/reflect2 v1.0.2 // indirect
58-
github.com/pelletier/go-toml/v2 v2.2.0 // indirect
63+
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
5964
github.com/pmezard/go-difflib v1.0.0 // indirect
6065
github.com/rs/xid v1.6.0 // indirect
6166
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
6267
github.com/ugorji/go/codec v1.2.12 // indirect
63-
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
68+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
69+
go.opentelemetry.io/contrib/bridges/otelzerolog v0.0.0-20240726205214-b0584291236a // indirect
6470
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
65-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
66-
go.opentelemetry.io/otel/trace v1.36.0 // indirect
67-
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
71+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
72+
go.opentelemetry.io/otel/log v0.15.0 // indirect
73+
go.opentelemetry.io/otel/log/logtest v0.15.0 // indirect
74+
go.opentelemetry.io/otel/trace v1.39.0 // indirect
75+
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
6876
golang.org/x/arch v0.7.0 // indirect
69-
golang.org/x/crypto v0.41.0 // indirect
70-
golang.org/x/net v0.43.0 // indirect
71-
golang.org/x/oauth2 v0.30.0 // indirect
72-
golang.org/x/sync v0.16.0 // indirect
73-
golang.org/x/sys v0.35.0 // indirect
74-
golang.org/x/text v0.28.0 // indirect
77+
golang.org/x/crypto v0.46.0 // indirect
78+
golang.org/x/net v0.48.0 // indirect
79+
golang.org/x/oauth2 v0.32.0 // indirect
80+
golang.org/x/sync v0.19.0 // indirect
81+
golang.org/x/sys v0.39.0 // indirect
82+
golang.org/x/text v0.32.0 // indirect
7583
golang.org/x/time v0.12.0 // indirect
7684
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
77-
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
78-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
79-
google.golang.org/grpc v1.74.2 // indirect
85+
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
86+
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
87+
google.golang.org/grpc v1.77.0 // indirect
8088
gopkg.in/yaml.v3 v3.0.1 // indirect
8189
)

0 commit comments

Comments
 (0)