ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Opentelemetry (#512)

* opentelemtery?

* use azure otel endpoint

* grafana cloud

* okay that kind of worked do i like grafana cloud

* get rid of operation id

* more traces in mail and secretref

* put user and session on top level trace

* simplify

* ensure headers for grafana

* still have traces when not exporting

* agents.md

---------

Co-authored-by: paul miller <paul.miller>

authored by

Paul Miller
paul miller
and committed by
GitHub
506bf29d 562477fd

+407 -437
+1 -1
AGENTS.md
··· 48 48 - Keep commits scoped and reviewable; avoid mixing refactors with feature changes unless necessary. 49 49 50 50 ## Security & Configuration Notes 51 - - Required env vars: `KROGER_CLIENT_ID`, `KROGER_CLIENT_SECRET`, `AI_API_KEY`; optional `GEMINI_API_KEY`, `GEMINI_CRITIQUE_MODEL`, `CLARITY_PROJECT_ID`, `GOOGLE_TAG_ID`, `GOOGLE_CONVERSION_LABEL`, `HISTORY_PATH`. Azure logging uses `AZURE_STORAGE_ACCOUNT_NAME` and `AZURE_STORAGE_PRIMARY_ACCOUNT_KEY`. 51 + - Required env vars: `KROGER_CLIENT_ID`, `KROGER_CLIENT_SECRET`, `AI_API_KEY`; optional `GEMINI_API_KEY`, `GEMINI_CRITIQUE_MODEL`, `CLARITY_PROJECT_ID`, `GOOGLE_TAG_ID`, `GOOGLE_CONVERSION_LABEL`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS`. Azure Blob cache still uses `AZURE_STORAGE_ACCOUNT_NAME` and `AZURE_STORAGE_PRIMARY_ACCOUNT_KEY`. Grafana Cloud direct OTLP uses the standard upstream OpenTelemetry endpoint and headers env vars. 52 52 - Never commit secrets or generated recipe outputs. If testing against real APIs, use minimal scopes and rotate keys promptly. 53 53 - Any handler that lets you see data from multiple users should go behind the /admin mux to secure it.
+4
README.md
··· 18 18 - `CLARITY_PROJECT_ID` - Microsoft Clarity project ID for web analytics (optional) 19 19 - `GOOGLE_TAG_ID` - Google Ads/gtag ID for web analytics (optional) 20 20 - `GOOGLE_CONVERSION_LABEL` - Google Ads conversion label used on `/auth/establish?signup=true` (optional) 21 + - `OTEL_EXPORTER_OTLP_ENDPOINT` - OTLP HTTP endpoint. For Grafana Cloud, use the endpoint from the OpenTelemetry connection tile. 22 + - `OTEL_EXPORTER_OTLP_HEADERS` - OTLP headers. For Grafana Cloud, use the generated `Authorization=Basic ...` header value from the OpenTelemetry connection tile. 21 23 - `SENDGRID_API_KEY` - To allow sending weekly recipe lists via email 22 24 - `ALBERTSONS_SEARCH_SUBSCRIPTION_KEY` - Albertsons-family pathway search subscription key 23 25 - `ALBERTSONS_SEARCH_REESE84` - fallback Albertsons-family `reese84` cookie when cache is empty or stale 24 26 - `BRIGHTDATA_BROWSER_WS_ENDPOINT` - Bright Data Browser API websocket endpoint for `cmd/albertsonsreese84`; may include embedded credentials 25 27 - `AZURE_STORAGE_ACCOUNT_NAME` and `AZURE_STORAGE_PRIMARY_ACCOUNT_KEY` - enable Azure Blob-backed cache storage 28 + 29 + For Grafana Cloud, the direct OTLP setup uses standard upstream OpenTelemetry env vars. Grafana's docs provide generated values for `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_HEADERS`. 26 30 27 31 if you're 28 32 - `ENABLE_MOCKS` - For testing if you have none of the above
+40 -125
cmd/careme/middleware.go
··· 1 1 package main 2 2 3 3 import ( 4 - "context" 5 - "errors" 6 4 "log/slog" 7 5 "net/http" 8 - "net/url" 9 - "os" 10 6 "runtime/debug" 11 - "strconv" 12 - "strings" 13 7 "time" 14 8 15 9 "careme/internal/logsetup" 16 10 17 11 "github.com/clerk/clerk-sdk-go/v2" 18 12 "github.com/google/uuid" 19 - azureappinsights "github.com/microsoft/ApplicationInsights-Go/appinsights" 20 - "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" 13 + "go.opentelemetry.io/otel" 14 + "go.opentelemetry.io/otel/attribute" 15 + "go.opentelemetry.io/otel/codes" 16 + "go.opentelemetry.io/otel/propagation" 17 + oteltrace "go.opentelemetry.io/otel/trace" 21 18 ) 22 19 23 20 const ( ··· 41 38 42 39 func (l *logger) ServeHTTP(w http.ResponseWriter, r *http.Request) { 43 40 start := time.Now() 44 - // should we use auth client? 45 41 user := "" 46 42 if claims, ok := clerk.SessionClaimsFromContext(r.Context()); ok { 47 43 user = claims.Subject ··· 53 49 slog.InfoContext(r.Context(), "request", "method", r.Method, "url", r.URL.Path, "query", r.URL.Query(), "response", lrw.statusCode, "user", user, "form", r.Form, "duration", time.Since(start)) 54 50 } 55 51 56 - type requestTracker interface { 57 - TrackRequest(ctx context.Context, method, url string, duration time.Duration, responseCode string) 58 - } 59 - 60 - type appInsightsTracker struct { 52 + type telemetryHandler struct { 61 53 http.Handler 62 - tracker requestTracker 54 + tracer oteltrace.Tracer 63 55 } 64 56 65 - const appInsightsIngestionPath = "/v2/track" 66 - 67 - type appInsightsTelemetryTracker struct { 68 - client azureappinsights.TelemetryClient 69 - } 70 - 71 - func (t *appInsightsTelemetryTracker) TrackRequest(ctx context.Context, method, url string, duration time.Duration, responseCode string) { 72 - request := azureappinsights.NewRequestTelemetry(method, url, duration, responseCode) 73 - tags := contracts.ContextTags(request.ContextTags()) 74 - if operationID, ok := logsetup.OperationIDFromContext(ctx); ok { 75 - tags.Operation().SetId(operationID) 76 - } 77 - if sessionID, ok := logsetup.SessionIDFromContext(ctx); ok { 78 - tags.Session().SetId(sessionID) 79 - } 80 - t.client.Track(request) 81 - } 82 - 83 - func (a *appInsightsTracker) ServeHTTP(w http.ResponseWriter, r *http.Request) { 84 - start := time.Now() 85 - lrw := &loggingResponseWriter{w, http.StatusOK} 86 - a.Handler.ServeHTTP(lrw, r) 87 - 88 - a.tracker.TrackRequest(r.Context(), r.Method, r.URL.String(), time.Since(start), strconv.Itoa(lrw.statusCode)) 89 - } 90 - 91 - func newAppInsightsTracker(next http.Handler, tracker requestTracker) http.Handler { 92 - if tracker == nil { 93 - return next 94 - } 95 - 96 - return &appInsightsTracker{ 57 + func newTelemetryHandler(next http.Handler) http.Handler { 58 + return &telemetryHandler{ 97 59 Handler: next, 98 - tracker: tracker, 60 + tracer: otel.Tracer("careme/http"), 99 61 } 100 62 } 101 63 102 - func newRequestTracker(connectionString string) (requestTracker, error) { 103 - client, err := newAppInsightsTelemetryClient(connectionString) 104 - if err != nil { 105 - return nil, err 106 - } 107 - return &appInsightsTelemetryTracker{client: client}, nil 108 - } 64 + func (t *telemetryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 65 + // will propogate if this is a service to service call 66 + ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header)) 109 67 110 - func newRequestTrackerFromEnv() requestTracker { 111 - connectionString := os.Getenv(logsetup.AppInsightsConnectionStringEnv) 112 - if connectionString == "" { 113 - return nil 68 + spanName := r.URL.Path 69 + if r.Pattern != "" { 70 + spanName = r.Pattern 114 71 } 115 72 116 - tracker, err := newRequestTracker(connectionString) 117 - if err != nil { 118 - slog.Error("failed to configure app insights request tracking", "error", err) 119 - return nil 73 + scheme := "http" 74 + if r.TLS != nil { 75 + scheme = "https" 120 76 } 121 77 122 - return tracker 123 - } 124 - 125 - func newAppInsightsTelemetryClient(connectionString string) (azureappinsights.TelemetryClient, error) { 126 - cfg, err := parseAppInsightsConnectionString(connectionString) 127 - if err != nil { 128 - return nil, err 78 + ctx, span := t.tracer.Start(ctx, spanName, oteltrace.WithSpanKind(oteltrace.SpanKindServer)) 79 + span.SetAttributes( 80 + attribute.String("http.method", r.Method), 81 + attribute.String("http.scheme", scheme), 82 + attribute.String("http.host", r.Host), 83 + attribute.String("http.target", r.URL.RequestURI()), 84 + ) 85 + if sessionID, ok := logsetup.SessionIDFromContext(ctx); ok { 86 + span.SetAttributes(attribute.String("session.id", sessionID)) 129 87 } 130 - return azureappinsights.NewTelemetryClientFromConfig(cfg), nil 131 - } 132 - 133 - // suprise there is not a parse function here. Chatgpt things github.com/Azure/go-autorest/autorest/azure.ParseConnectionString but codex coudln't find it 134 - func parseAppInsightsConnectionString(connectionString string) (*azureappinsights.TelemetryConfiguration, error) { 135 - connectionString = strings.TrimSpace(connectionString) 136 - if connectionString == "" { 137 - return nil, errors.New("connection string is empty") 88 + if claims, ok := clerk.SessionClaimsFromContext(ctx); ok && claims != nil && claims.Subject != "" { 89 + span.SetAttributes(attribute.String("enduser.id", claims.Subject)) 138 90 } 139 91 140 - var instrumentationKey string 141 - var ingestionEndpoint string 142 - 143 - for _, value := range strings.Split(connectionString, ";") { 144 - pair := strings.SplitN(value, "=", 2) 145 - if len(pair) != 2 { 146 - continue 147 - } 148 - switch pair[0] { 149 - case "InstrumentationKey": 150 - instrumentationKey = pair[1] 151 - case "IngestionEndpoint": 152 - ingestionEndpoint = pair[1] 153 - } 154 - } 92 + lrw := &loggingResponseWriter{w, http.StatusOK} 93 + t.Handler.ServeHTTP(lrw, r.WithContext(ctx)) 155 94 156 - if instrumentationKey == "" { 157 - return nil, errors.New("instrumentation key is missing") 95 + span.SetAttributes(attribute.Int("http.status_code", lrw.statusCode)) 96 + if r.Pattern != "" { 97 + span.SetAttributes(attribute.String("http.route", r.Pattern)) 158 98 } 159 - if ingestionEndpoint == "" { 160 - return nil, errors.New("ingestion endpoint is missing") 99 + if lrw.statusCode >= http.StatusBadRequest { 100 + span.SetStatus(codes.Error, http.StatusText(lrw.statusCode)) 161 101 } 162 - 163 - ingestionURL, err := url.Parse(ingestionEndpoint) 164 - if err != nil { 165 - return nil, err 166 - } 167 - 168 - cfg := azureappinsights.NewTelemetryConfiguration(instrumentationKey) 169 - ingestionURL.Path = appInsightsIngestionPath 170 - cfg.EndpointUrl = ingestionURL.String() 171 - return cfg, nil 102 + span.End() 172 103 } 173 104 174 105 type recoverer struct { ··· 176 107 } 177 108 178 109 func (r *recoverer) ServeHTTP(w http.ResponseWriter, req *http.Request) { 179 - // app insights could also track this https://github.com/microsoft/ApplicationInsights-Go?tab=readme-ov-file#exceptions 180 110 defer func() { 181 111 if err := recover(); err != nil { 182 112 slog.ErrorContext(req.Context(), "panic recovered", "error", err, "stack", debug.Stack()) ··· 184 114 } 185 115 }() 186 116 r.Handler.ServeHTTP(w, req) 187 - } 188 - 189 - type operationIDHandler struct { 190 - http.Handler 191 - } 192 - 193 - // extract or generate an operation ID for the request, add it to the context, and set it in the response header. The operation ID is used for correlating logs and telemetry. 194 - func (h *operationIDHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 195 - operationID := uuid.NewString() 196 - ctx := logsetup.WithOperationID(r.Context(), operationID) 197 - w.Header().Set("X-Operation-ID", operationID) 198 - h.Handler.ServeHTTP(w, r.WithContext(ctx)) 199 117 } 200 118 201 119 type sessionIDHandler struct { ··· 232 150 } 233 151 } 234 152 235 - // just recover and log 236 153 func baseMiddleware(h http.Handler) http.Handler { 237 154 h = &recoverer{h} 238 155 return &logger{h} 239 156 } 240 157 241 - // instrument with app insights and log with operation and session ids. 242 - func appMiddleware(h http.Handler, tracker requestTracker) http.Handler { 158 + func appMiddleware(h http.Handler) http.Handler { 243 159 h = baseMiddleware(h) 244 - h = newAppInsightsTracker(h, tracker) // must be "inside" operatid and session handler. 245 - h = &operationIDHandler{h} 160 + h = newTelemetryHandler(h) 246 161 return &sessionIDHandler{h} 247 162 }
+97 -207
cmd/careme/middleware_test.go
··· 4 4 "context" 5 5 "net/http" 6 6 "net/http/httptest" 7 - "strings" 8 7 "testing" 9 - "time" 10 8 11 9 "careme/internal/logsetup" 12 - ) 13 10 14 - type trackedRequest struct { 15 - method string 16 - url string 17 - duration time.Duration 18 - responseCode string 19 - operationID string 20 - sessionID string 21 - } 22 - 23 - type fakeRequestTracker struct { 24 - calls []trackedRequest 25 - } 11 + "github.com/clerk/clerk-sdk-go/v2" 12 + "go.opentelemetry.io/otel" 13 + "go.opentelemetry.io/otel/attribute" 14 + "go.opentelemetry.io/otel/codes" 15 + "go.opentelemetry.io/otel/propagation" 16 + sdktrace "go.opentelemetry.io/otel/sdk/trace" 17 + "go.opentelemetry.io/otel/sdk/trace/tracetest" 18 + oteltrace "go.opentelemetry.io/otel/trace" 19 + "go.opentelemetry.io/otel/trace/noop" 20 + ) 26 21 27 - func (f *fakeRequestTracker) TrackRequest(ctx context.Context, method, url string, duration time.Duration, responseCode string) { 28 - operationID, _ := logsetup.OperationIDFromContext(ctx) 29 - sessionID, _ := logsetup.SessionIDFromContext(ctx) 30 - f.calls = append(f.calls, trackedRequest{ 31 - method: method, 32 - url: url, 33 - duration: duration, 34 - responseCode: responseCode, 35 - operationID: operationID, 36 - sessionID: sessionID, 37 - }) 38 - } 22 + func TestTelemetryHandlerRecordsResponseCode(t *testing.T) { 23 + recorder := installTestTracerProvider(t) 39 24 40 - func TestAppInsightsTrackerTracksResponseCode(t *testing.T) { 41 - tracker := &fakeRequestTracker{} 42 - mw := &appInsightsTracker{ 43 - Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 44 - w.WriteHeader(http.StatusCreated) 45 - }), 46 - tracker: tracker, 47 - } 25 + handler := newTelemetryHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 26 + w.WriteHeader(http.StatusCreated) 27 + })) 48 28 49 29 req := httptest.NewRequest(http.MethodPost, "https://careme.cooking/recipes?vegan=true", nil) 50 30 rec := httptest.NewRecorder() 51 - mw.ServeHTTP(rec, req) 31 + handler.ServeHTTP(rec, req) 52 32 53 - if len(tracker.calls) != 1 { 54 - t.Fatalf("expected 1 tracked request, got %d", len(tracker.calls)) 33 + spans := recorder.Ended() 34 + if len(spans) != 1 { 35 + t.Fatalf("expected 1 span, got %d", len(spans)) 55 36 } 56 37 57 - call := tracker.calls[0] 58 - if call.method != http.MethodPost { 59 - t.Fatalf("expected method %q, got %q", http.MethodPost, call.method) 38 + span := spans[0] 39 + if span.Name() != "/recipes" { 40 + t.Fatalf("expected span name %q, got %q", "/recipes", span.Name()) 60 41 } 61 - if call.url != req.URL.String() { 62 - t.Fatalf("expected url %q, got %q", req.URL.String(), call.url) 42 + attrs := spanAttributes(span) 43 + if got := attrs["http.method"].AsString(); got != http.MethodPost { 44 + t.Fatalf("expected http.method %q, got %q", http.MethodPost, got) 63 45 } 64 - if call.responseCode != "201" { 65 - t.Fatalf("expected response code 201, got %q", call.responseCode) 66 - } 67 - if call.duration <= 0 { 68 - t.Fatalf("expected positive duration, got %s", call.duration) 46 + if got := int(attrs["http.status_code"].AsInt64()); got != http.StatusCreated { 47 + t.Fatalf("expected http.status_code %d, got %d", http.StatusCreated, got) 69 48 } 70 49 } 71 50 72 - func TestAppInsightsTrackerDefaultsStatusCodeTo200(t *testing.T) { 73 - tracker := &fakeRequestTracker{} 74 - mw := &appInsightsTracker{ 75 - Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 76 - _, _ = w.Write([]byte("ok")) 77 - }), 78 - tracker: tracker, 79 - } 80 - 81 - req := httptest.NewRequest(http.MethodGet, "https://careme.cooking/about", nil) 82 - rec := httptest.NewRecorder() 83 - mw.ServeHTTP(rec, req) 84 - 85 - if len(tracker.calls) != 1 { 86 - t.Fatalf("expected 1 tracked request, got %d", len(tracker.calls)) 87 - } 88 - if tracker.calls[0].responseCode != "200" { 89 - t.Fatalf("expected response code 200, got %q", tracker.calls[0].responseCode) 90 - } 91 - } 51 + func TestTelemetryHandlerRecordsRecoveredPanicAs500(t *testing.T) { 52 + recorder := installTestTracerProvider(t) 92 53 93 - func TestAppInsightsTrackerTracksRecoveredPanicAs500(t *testing.T) { 94 - tracker := &fakeRequestTracker{} 95 - mw := &appInsightsTracker{ 96 - Handler: &recoverer{ 97 - Handler: http.HandlerFunc(func(http.ResponseWriter, *http.Request) { 98 - panic("boom") 99 - }), 100 - }, 101 - tracker: tracker, 102 - } 54 + handler := newTelemetryHandler(&recoverer{ 55 + Handler: http.HandlerFunc(func(http.ResponseWriter, *http.Request) { 56 + panic("boom") 57 + }), 58 + }) 103 59 104 60 req := httptest.NewRequest(http.MethodGet, "https://careme.cooking/panic", nil) 105 61 rec := httptest.NewRecorder() 106 - mw.ServeHTTP(rec, req) 62 + handler.ServeHTTP(rec, req) 107 63 108 64 if rec.Code != http.StatusInternalServerError { 109 65 t.Fatalf("expected status 500, got %d", rec.Code) 110 - } 111 - if len(tracker.calls) != 1 { 112 - t.Fatalf("expected 1 tracked request, got %d", len(tracker.calls)) 113 - } 114 - if tracker.calls[0].responseCode != "500" { 115 - t.Fatalf("expected response code 500, got %q", tracker.calls[0].responseCode) 116 - } 117 - } 118 - 119 - func TestAppInsightsTrackerReusesOperationIDFromContext(t *testing.T) { 120 - tracker := &fakeRequestTracker{} 121 - mw := &appInsightsTracker{ 122 - Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 123 - w.WriteHeader(http.StatusAccepted) 124 - }), 125 - tracker: tracker, 126 - } 127 - 128 - req := httptest.NewRequest(http.MethodGet, "https://careme.cooking/about", nil) 129 - req = req.WithContext(logsetup.WithOperationID(req.Context(), "op-555")) 130 - rec := httptest.NewRecorder() 131 - mw.ServeHTTP(rec, req) 132 - 133 - if len(tracker.calls) != 1 { 134 - t.Fatalf("expected 1 tracked request, got %d", len(tracker.calls)) 135 - } 136 - if tracker.calls[0].operationID != "op-555" { 137 - t.Fatalf("expected tracker to receive operation id op-555, got %q", tracker.calls[0].operationID) 138 - } 139 - } 140 - 141 - func TestAppInsightsTrackerIncludesSessionIDFromContext(t *testing.T) { 142 - tracker := &fakeRequestTracker{} 143 - mw := &appInsightsTracker{ 144 - Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 145 - w.WriteHeader(http.StatusAccepted) 146 - }), 147 - tracker: tracker, 148 66 } 149 67 150 - req := httptest.NewRequest(http.MethodGet, "https://careme.cooking/about", nil) 151 - req = req.WithContext(logsetup.WithSessionID(req.Context(), "sess-555")) 152 - rec := httptest.NewRecorder() 153 - mw.ServeHTTP(rec, req) 154 - 155 - if len(tracker.calls) != 1 { 156 - t.Fatalf("expected 1 tracked request, got %d", len(tracker.calls)) 68 + spans := recorder.Ended() 69 + if len(spans) != 1 { 70 + t.Fatalf("expected 1 span, got %d", len(spans)) 157 71 } 158 - if tracker.calls[0].sessionID != "sess-555" { 159 - t.Fatalf("expected tracker to receive session id sess-555, got %q", tracker.calls[0].sessionID) 72 + if spans[0].Status().Code != codes.Error { 73 + t.Fatalf("expected span status %v, got %v", codes.Error, spans[0].Status().Code) 160 74 } 161 75 } 162 76 ··· 249 163 } 250 164 } 251 165 252 - func TestWithMiddlewareProvidesBothIDs(t *testing.T) { 253 - var operationID string 166 + func TestWithMiddlewareProvidesTraceAndSessionContext(t *testing.T) { 167 + recorder := installTestTracerProvider(t) 168 + 254 169 var sessionID string 170 + var traceID string 255 171 handler := appMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 256 - operationID, _ = logsetup.OperationIDFromContext(r.Context()) 257 172 sessionID, _ = logsetup.SessionIDFromContext(r.Context()) 173 + traceID = oteltrace.SpanContextFromContext(r.Context()).TraceID().String() 258 174 w.WriteHeader(http.StatusNoContent) 259 - }), &fakeRequestTracker{}) 175 + })) 260 176 261 177 req := httptest.NewRequest(http.MethodGet, "http://careme.cooking/about", nil) 178 + req = req.WithContext(clerk.ContextWithSessionClaims(req.Context(), &clerk.SessionClaims{ 179 + RegisteredClaims: clerk.RegisteredClaims{ 180 + Subject: "user-123", 181 + }, 182 + })) 262 183 rec := httptest.NewRecorder() 263 184 handler.ServeHTTP(rec, req) 264 185 265 - if operationID == "" { 266 - t.Fatal("expected operation id in context") 186 + if traceID == "" { 187 + t.Fatal("expected trace id in context") 267 188 } 268 189 if sessionID == "" { 269 190 t.Fatal("expected session id in context") 270 191 } 271 - if rec.Header().Get("X-Operation-ID") != operationID { 272 - t.Fatalf("expected X-Operation-ID %q, got %q", operationID, rec.Header().Get("X-Operation-ID")) 273 - } 274 192 cookie := findCookie(t, rec.Result().Cookies(), sessionCookieName) 275 193 if cookie.Value != sessionID { 276 194 t.Fatalf("expected session cookie %q, got %q", sessionID, cookie.Value) 277 195 } 196 + spans := recorder.Ended() 197 + if len(spans) != 1 { 198 + t.Fatalf("expected 1 span, got %d", len(spans)) 199 + } 200 + attrs := spanAttributes(spans[0]) 201 + if got := attrs["session.id"].AsString(); got != sessionID { 202 + t.Fatalf("expected session.id %q, got %q", sessionID, got) 203 + } 204 + if got := attrs["enduser.id"].AsString(); got != "user-123" { 205 + t.Fatalf("expected enduser.id %q, got %q", "user-123", got) 206 + } 278 207 } 279 208 280 - func TestWithMiddlewareProvidesIDsWithoutTracker(t *testing.T) { 281 - var operationID string 282 - var sessionID string 209 + func TestWithMiddlewarePreservesIncomingTraceContext(t *testing.T) { 210 + installTestTracerProvider(t) 211 + 212 + var traceID string 283 213 handler := appMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 284 - operationID, _ = logsetup.OperationIDFromContext(r.Context()) 285 - sessionID, _ = logsetup.SessionIDFromContext(r.Context()) 214 + traceID = oteltrace.SpanContextFromContext(r.Context()).TraceID().String() 286 215 w.WriteHeader(http.StatusNoContent) 287 - }), nil) 216 + })) 288 217 289 218 req := httptest.NewRequest(http.MethodGet, "http://careme.cooking/about", nil) 219 + req = req.WithContext(oteltrace.ContextWithSpanContext(req.Context(), oteltrace.NewSpanContext(oteltrace.SpanContextConfig{ 220 + TraceID: oteltrace.TraceID{1, 2, 3}, 221 + SpanID: oteltrace.SpanID{4, 5, 6}, 222 + TraceFlags: oteltrace.FlagsSampled, 223 + Remote: true, 224 + }))) 290 225 rec := httptest.NewRecorder() 291 226 handler.ServeHTTP(rec, req) 292 227 293 - if rec.Code != http.StatusNoContent { 294 - t.Fatalf("expected status 204, got %d", rec.Code) 295 - } 296 - if operationID == "" { 297 - t.Fatal("expected operation id in context") 298 - } 299 - if sessionID == "" { 300 - t.Fatal("expected session id in context") 301 - } 302 - if rec.Header().Get("X-Operation-ID") != operationID { 303 - t.Fatalf("expected X-Operation-ID %q, got %q", operationID, rec.Header().Get("X-Operation-ID")) 304 - } 305 - cookie := findCookie(t, rec.Result().Cookies(), sessionCookieName) 306 - if cookie.Value != sessionID { 307 - t.Fatalf("expected session cookie %q, got %q", sessionID, cookie.Value) 228 + if traceID != "01020300000000000000000000000000" { 229 + t.Fatalf("expected preserved trace id, got %q", traceID) 308 230 } 309 231 } 310 232 ··· 331 253 w.WriteHeader(http.StatusNoContent) 332 254 }) 333 255 rootMux.Handle("/static/", baseMiddleware(infraMux)) 334 - rootMux.Handle("/", appMiddleware(appMux, &fakeRequestTracker{})) 256 + rootMux.Handle("/", appMiddleware(appMux)) 335 257 336 258 staticReq := httptest.NewRequest(http.MethodGet, "http://careme.cooking/static/app.js", nil) 337 259 staticRec := httptest.NewRecorder() ··· 353 275 } 354 276 } 355 277 356 - func TestParseAppInsightsConnectionString(t *testing.T) { 357 - connectionString := "InstrumentationKey=test-key;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=app-id" 358 - cfg, err := parseAppInsightsConnectionString(connectionString) 359 - if err != nil { 360 - t.Fatalf("unexpected error: %v", err) 361 - } 362 - if cfg.InstrumentationKey != "test-key" { 363 - t.Fatalf("expected instrumentation key test-key, got %q", cfg.InstrumentationKey) 364 - } 365 - if cfg.EndpointUrl != "https://westus3-1.in.applicationinsights.azure.com/v2/track" { 366 - t.Fatalf("unexpected ingestion endpoint: %q", cfg.EndpointUrl) 367 - } 368 - } 278 + func installTestTracerProvider(t *testing.T) *tracetest.SpanRecorder { 279 + t.Helper() 369 280 370 - func TestParseAppInsightsConnectionStringErrors(t *testing.T) { 371 - testCases := []struct { 372 - name string 373 - value string 374 - wantErrText string 375 - }{ 376 - { 377 - name: "empty", 378 - value: "", 379 - wantErrText: "connection string is empty", 380 - }, 381 - { 382 - name: "missing instrumentation key", 383 - value: "IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/", 384 - wantErrText: "instrumentation key is missing", 385 - }, 386 - { 387 - name: "missing ingestion endpoint", 388 - value: "InstrumentationKey=test-key", 389 - wantErrText: "ingestion endpoint is missing", 390 - }, 391 - { 392 - name: "bad ingestion endpoint", 393 - value: "InstrumentationKey=test-key;IngestionEndpoint=:bad://", 394 - wantErrText: "missing protocol scheme", 395 - }, 396 - } 281 + recorder := tracetest.NewSpanRecorder() 282 + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder)) 283 + otel.SetTracerProvider(provider) 284 + otel.SetTextMapPropagator(propagation.TraceContext{}) 285 + t.Cleanup(func() { 286 + _ = provider.Shutdown(context.Background()) 287 + otel.SetTracerProvider(noop.NewTracerProvider()) 288 + }) 397 289 398 - for _, tc := range testCases { 399 - t.Run(tc.name, func(t *testing.T) { 400 - _, err := parseAppInsightsConnectionString(tc.value) 401 - if err == nil { 402 - t.Fatalf("expected error containing %q", tc.wantErrText) 403 - } 404 - if !strings.Contains(err.Error(), tc.wantErrText) { 405 - t.Fatalf("expected error containing %q, got %q", tc.wantErrText, err.Error()) 406 - } 407 - }) 290 + return recorder 291 + } 292 + 293 + func spanAttributes(span sdktrace.ReadOnlySpan) map[string]attribute.Value { 294 + attrs := make(map[string]attribute.Value, len(span.Attributes())) 295 + for _, attr := range span.Attributes() { 296 + attrs[string(attr.Key)] = attr.Value 408 297 } 298 + return attrs 409 299 }
-3
cmd/careme/ready.go
··· 6 6 "log/slog" 7 7 "net/http" 8 8 "sync" 9 - 10 - "careme/internal/logsetup" 11 9 ) 12 10 13 11 type readyOnce struct { ··· 22 20 if r.done { 23 21 return nil 24 22 } 25 - ctx = logsetup.WithOperationID(ctx, "readiness_check") 26 23 for _, check := range r.checks { 27 24 if err := check.Ready(ctx); err != nil { 28 25 slog.ErrorContext(ctx, "check failed", "error", err, "check", fmt.Sprintf("%T", check))
+1 -1
cmd/careme/web.go
··· 47 47 48 48 rootMux := http.NewServeMux() 49 49 appRoutes := routing.Wrap(rootMux, func(h http.Handler) http.Handler { 50 - return authClient.WithAuthHTTP(appMiddleware(h, newRequestTrackerFromEnv())) 50 + return authClient.WithAuthHTTP(appMiddleware(h)) 51 51 }) 52 52 infraRoutes := routing.Wrap(rootMux, baseMiddleware) 53 53
+1 -1
cmd/careme/web_e2e_test.go
··· 197 197 198 198 rootMux := http.NewServeMux() 199 199 appRoutes := routing.Wrap(rootMux, func(h http.Handler) http.Handler { 200 - return mockAuth.WithAuthHTTP(appMiddleware(h, &fakeRequestTracker{})) 200 + return mockAuth.WithAuthHTTP(appMiddleware(h)) 201 201 }) 202 202 infraRoutes := routing.Wrap(rootMux, baseMiddleware) 203 203 locationServer := locations.NewServer(locationStorage, centroids, userStorage)
+2 -3
deploy/cronjob-albertsons-reese84.yaml
··· 29 29 imagePullPolicy: IfNotPresent 30 30 envFrom: 31 31 - secretRef: 32 + name: grafana 33 + - secretRef: 32 34 name: storage 33 35 - secretRef: 34 36 name: brightdata-proxy 35 - env: 36 - - name: APPLICATIONINSIGHTS_CONNECTION_STRING 37 - value: "InstrumentationKey=a532fcc7-5098-4f44-8dde-ff2f32d6a59b;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=fdc94780-6135-4a29-980e-ab114a402e58" 38 37 args: ["wait-ms", "10000"] 39 38 resources: 40 39 requests:
+2 -3
deploy/cronjob-albertsons-scrape.yaml
··· 29 29 imagePullPolicy: IfNotPresent 30 30 envFrom: 31 31 - secretRef: 32 + name: grafana 33 + - secretRef: 32 34 name: storage 33 - env: 34 - - name: APPLICATIONINSIGHTS_CONNECTION_STRING 35 - value: "InstrumentationKey=a532fcc7-5098-4f44-8dde-ff2f32d6a59b;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=fdc94780-6135-4a29-980e-ab114a402e58" 36 35 resources: 37 36 requests: 38 37 cpu: 50m
+2 -2
deploy/cronjob-careme-mail.yaml
··· 34 34 - secretRef: 35 35 name: auth 36 36 - secretRef: 37 + name: grafana 38 + - secretRef: 37 39 name: mail 38 40 - secretRef: 39 41 name: kroger ··· 42 44 - secretRef: 43 45 name: walmart 44 46 env: 45 - - name: APPLICATIONINSIGHTS_CONNECTION_STRING 46 - value: "InstrumentationKey=a532fcc7-5098-4f44-8dde-ff2f32d6a59b;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=fdc94780-6135-4a29-980e-ab114a402e58" 47 47 - name: PUBLIC_ORIGIN 48 48 value: "https://careme.cooking" 49 49 resources:
+2 -3
deploy/cronjob-wholefoods-scrape.yaml
··· 29 29 imagePullPolicy: IfNotPresent 30 30 envFrom: 31 31 - secretRef: 32 + name: grafana 33 + - secretRef: 32 34 name: storage 33 - env: 34 - - name: APPLICATIONINSIGHTS_CONNECTION_STRING 35 - value: "InstrumentationKey=a532fcc7-5098-4f44-8dde-ff2f32d6a59b;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=fdc94780-6135-4a29-980e-ab114a402e58" 36 35 resources: 37 36 requests: 38 37 cpu: 50m
+2 -2
deploy/deploy.yaml
··· 36 36 - secretRef: 37 37 name: brightdata-proxy 38 38 - secretRef: 39 + name: grafana 40 + - secretRef: 39 41 name: kroger 40 42 - secretRef: 41 43 name: storage ··· 52 54 value: "paul.miller@gmail.com" 53 55 - name: PUBLIC_ORIGIN 54 56 value: "https://careme.cooking" 55 - - name: APPLICATIONINSIGHTS_CONNECTION_STRING 56 - value: "InstrumentationKey=a532fcc7-5098-4f44-8dde-ff2f32d6a59b;IngestionEndpoint=https://westus3-1.in.applicationinsights.azure.com/;LiveEndpoint=https://westus3.livediagnostics.monitor.azure.com/;ApplicationId=fdc94780-6135-4a29-980e-ab114a402e58" 57 57 58 58 volumeMounts: 59 59 - name: recipes
+22 -9
go.mod
··· 15 15 github.com/hashicorp/go-retryablehttp v0.7.8 16 16 github.com/invopop/jsonschema v0.13.0 17 17 github.com/joho/godotenv v1.5.1 18 - github.com/microsoft/ApplicationInsights-Go v0.4.4 19 18 github.com/openai/openai-go/v3 v3.31.0 20 - github.com/openclosed-dev/slogan v0.2.0 21 19 github.com/sendgrid/rest v2.6.9+incompatible 22 20 github.com/sendgrid/sendgrid-go v3.16.1+incompatible 23 21 github.com/stretchr/testify v1.11.1 22 + go.opentelemetry.io/contrib/bridges/otelslog v0.18.0 23 + go.opentelemetry.io/otel v1.43.0 24 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 25 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 26 + go.opentelemetry.io/otel/log v0.19.0 27 + go.opentelemetry.io/otel/sdk v1.43.0 28 + go.opentelemetry.io/otel/sdk/log v0.19.0 29 + go.opentelemetry.io/otel/trace v1.43.0 24 30 golang.org/x/crypto v0.49.0 25 31 golang.org/x/net v0.52.0 26 32 golang.org/x/sync v0.20.0 ··· 33 39 require ( 34 40 cloud.google.com/go v0.116.0 // indirect 35 41 cloud.google.com/go/auth v0.9.3 // indirect 36 - cloud.google.com/go/compute/metadata v0.5.0 // indirect 37 - code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c // indirect 42 + cloud.google.com/go/compute/metadata v0.9.0 // indirect 38 43 filippo.io/edwards25519 v1.1.0 // indirect 39 44 github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect 40 45 github.com/bahlo/generic-list-go v0.2.0 // indirect 41 46 github.com/buger/jsonparser v1.1.1 // indirect 47 + github.com/cenkalti/backoff/v5 v5.0.3 // indirect 48 + github.com/cespare/xxhash/v2 v2.3.0 // indirect 42 49 github.com/davecgh/go-spew v1.1.1 // indirect 43 50 github.com/emicklei/go-restful/v3 v3.12.2 // indirect 44 51 github.com/fxamacker/cbor/v2 v2.9.0 // indirect 45 52 github.com/go-jose/go-jose/v3 v3.0.4 // indirect 46 53 github.com/go-logr/logr v1.4.3 // indirect 54 + github.com/go-logr/stdr v1.2.2 // indirect 47 55 github.com/go-openapi/jsonreference v0.20.2 // indirect 48 56 github.com/gobwas/httphead v0.1.0 // indirect 49 57 github.com/gobwas/pool v0.2.1 // indirect 50 - github.com/gofrs/uuid v3.3.0+incompatible // indirect 51 58 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 52 59 github.com/google/gnostic-models v0.7.0 // indirect 53 60 github.com/google/go-cmp v0.7.0 // indirect 54 61 github.com/google/s2a-go v0.1.8 // indirect 55 62 github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 56 63 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect 64 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect 57 65 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 58 66 github.com/json-iterator/go v1.1.12 // indirect 59 67 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect ··· 65 73 github.com/woodsbury/decimal128 v1.3.0 // indirect 66 74 github.com/x448/float16 v0.8.4 // indirect 67 75 go.opencensus.io v0.24.0 // indirect 76 + go.opentelemetry.io/auto/sdk v1.2.1 // indirect 77 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect 78 + go.opentelemetry.io/otel/metric v1.43.0 // indirect 79 + go.opentelemetry.io/proto/otlp v1.10.0 // indirect 68 80 go.yaml.in/yaml/v2 v2.4.3 // indirect 69 81 go.yaml.in/yaml/v3 v3.0.4 // indirect 70 - golang.org/x/oauth2 v0.30.0 // indirect 82 + golang.org/x/oauth2 v0.35.0 // indirect 71 83 golang.org/x/sys v0.42.0 // indirect 72 84 golang.org/x/term v0.41.0 // indirect 73 85 golang.org/x/time v0.9.0 // indirect 74 - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect 75 - google.golang.org/grpc v1.66.2 // indirect 76 - google.golang.org/protobuf v1.36.8 // indirect 86 + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect 87 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect 88 + google.golang.org/grpc v1.80.0 // indirect 89 + google.golang.org/protobuf v1.36.11 // indirect 77 90 gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect 78 91 gopkg.in/inf.v0 v0.9.1 // indirect 79 92 k8s.io/klog/v2 v2.130.1 // indirect
+55 -24
go.sum
··· 5 5 cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= 6 6 cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U= 7 7 cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= 8 - cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= 9 - cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= 10 - code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c h1:5eeuG0BHx1+DHeT3AP+ISKZ2ht1UjGhm581ljqYpVeQ= 11 - code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8= 8 + cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= 9 + cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= 12 10 filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= 13 11 filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= 14 12 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= ··· 36 34 github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= 37 35 github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 38 36 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 37 + github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= 38 + github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 39 39 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 40 + github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 41 + github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 40 42 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 41 43 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 42 44 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= ··· 68 70 github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= 69 71 github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= 70 72 github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 73 + github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 71 74 github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 72 75 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 76 + github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 77 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 73 78 github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 74 79 github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 75 80 github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= ··· 90 95 github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 91 96 github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= 92 97 github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= 93 - github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= 94 - github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 95 98 github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= 96 99 github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 97 100 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= ··· 111 114 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 112 115 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 113 116 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 117 + github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 118 + github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 114 119 github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= 115 120 github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 116 121 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= ··· 136 141 github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= 137 142 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= 138 143 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= 144 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= 145 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= 139 146 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 140 147 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 141 148 github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= ··· 169 176 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 170 177 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 171 178 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 172 - github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81TI5Es90b2t/MwX5KqY= 173 - github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U= 174 179 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 175 180 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 176 181 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= ··· 193 198 github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= 194 199 github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 195 200 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 196 - github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 197 201 github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 198 202 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 199 203 github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= ··· 201 205 github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 202 206 github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= 203 207 github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= 204 - github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 205 208 github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 206 209 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 207 210 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= ··· 209 212 github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 210 213 github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= 211 214 github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= 212 - github.com/openai/openai-go/v3 v3.29.0 h1:dZNJ0w7DxwpgppzKQjSKfLebW27KrtGqgSy4ipJS0U8= 213 - github.com/openai/openai-go/v3 v3.29.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= 214 215 github.com/openai/openai-go/v3 v3.31.0 h1:3KxL3H+gw6vBkBW6dmcwhbFqP4kyMgmaWTsuRheyF8w= 215 216 github.com/openai/openai-go/v3 v3.31.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= 216 - github.com/openclosed-dev/slogan v0.2.0 h1:Nh1z0IJ366ADFqu5pZY7SdMcYeONaeCx2J5Od9xHSfs= 217 - github.com/openclosed-dev/slogan v0.2.0/go.mod h1:1c/tiM++o7TnyP1WNnbnMecG2Yjs7kGmkXmVj8WzElg= 218 217 github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 219 218 github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 220 219 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= ··· 253 252 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 254 253 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 255 254 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 256 - github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0= 257 255 github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 258 256 github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 259 257 github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= ··· 278 276 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 279 277 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 280 278 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 279 + go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= 280 + go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 281 + go.opentelemetry.io/contrib/bridges/otelslog v0.18.0 h1:hhPGP3zvvy1xWT9RTy970wlniSxFttBIsAK1gvMguJM= 282 + go.opentelemetry.io/contrib/bridges/otelslog v0.18.0/go.mod h1:twJF7inoMza6kxMcF8JOdL3mPmtOZu7GEr34CUNE6Dg= 283 + go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= 284 + go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= 285 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= 286 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= 287 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= 288 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= 289 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= 290 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= 291 + go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= 292 + go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= 293 + go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= 294 + go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= 295 + go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= 296 + go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= 297 + go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= 298 + go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= 299 + go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= 300 + go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= 301 + go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= 302 + go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= 303 + go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= 304 + go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= 305 + go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= 306 + go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= 307 + go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 308 + go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 281 309 go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= 282 310 go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 283 311 go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= ··· 317 345 golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= 318 346 golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= 319 347 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 320 - golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 321 - golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 348 + golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= 349 + golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 322 350 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 323 351 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 324 352 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ··· 383 411 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 384 412 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 385 413 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 414 + gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= 415 + gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= 386 416 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 387 417 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 388 418 google.golang.org/genai v1.53.0 h1:8tR9MuO/TdaXSc8PEFamohQKxRz5M/qctbyzhV2YwMM= ··· 390 420 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 391 421 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 392 422 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 393 - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= 394 - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 423 + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= 424 + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= 425 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= 426 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= 395 427 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 396 428 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 397 429 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 398 430 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 399 431 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 400 - google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= 401 - google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= 432 + google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= 433 + google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= 402 434 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 403 435 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 404 436 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= ··· 410 442 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 411 443 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 412 444 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 413 - google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 414 - google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 445 + google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= 446 + google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 415 447 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 416 - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 417 448 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 418 449 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 419 450 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-1
internal/ai/client.go
··· 313 313 }, nil 314 314 } 315 315 316 - // remove this and imageUsage if we get https://github.com/openclosed-dev/slogan/pull/3 in 317 316 func responseUsageLogAttr(usage responses.ResponseUsage) slog.Attr { 318 317 return slog.Group("usage", 319 318 slog.Int64("inputTokens", usage.InputTokens),
-1
internal/ai/critique.go
··· 127 127 return critique, nil 128 128 } 129 129 130 - // remove if we get https://github.com/openclosed-dev/slogan/pull/3 in 131 130 func geminiUsageLogAttr(usage *genai.GenerateContentResponseUsageMetadata) slog.Attr { 132 131 if usage == nil { 133 132 return slog.Group("usage", slog.Bool("available", false))
+9 -23
internal/logsetup/context.go
··· 5 5 "log/slog" 6 6 7 7 "github.com/clerk/clerk-sdk-go/v2" 8 + "go.opentelemetry.io/otel/trace" 8 9 ) 9 10 10 11 type contextKey string 11 12 12 13 const ( 13 - operationIDContextKey contextKey = "operation_id" 14 - sessionIDContextKey contextKey = "session_id" 14 + sessionIDContextKey contextKey = "session_id" 15 15 ) 16 - 17 - func WithOperationID(ctx context.Context, operationID string) context.Context { 18 - if operationID == "" { 19 - return ctx 20 - } 21 - return context.WithValue(ctx, operationIDContextKey, operationID) 22 - } 23 - 24 - func OperationIDFromContext(ctx context.Context) (string, bool) { 25 - if ctx == nil { 26 - return "", false 27 - } 28 - operationID, ok := ctx.Value(operationIDContextKey).(string) 29 - if !ok || operationID == "" { 30 - return "", false 31 - } 32 - return operationID, true 33 - } 34 16 35 17 func WithSessionID(ctx context.Context, sessionID string) context.Context { 36 18 if sessionID == "" { ··· 64 46 } 65 47 66 48 func (h *contextHandler) Handle(ctx context.Context, record slog.Record) error { 67 - if operationID, ok := OperationIDFromContext(ctx); ok { 68 - record.AddAttrs(slog.String("operation_id", operationID)) 69 - } 70 49 if sessionID, ok := SessionIDFromContext(ctx); ok { 71 50 record.AddAttrs(slog.String("session_id", sessionID)) 51 + } 52 + spanContext := trace.SpanContextFromContext(ctx) 53 + if spanContext.IsValid() { 54 + record.AddAttrs( 55 + slog.String("trace_id", spanContext.TraceID().String()), 56 + slog.String("span_id", spanContext.SpanID().String()), 57 + ) 72 58 } 73 59 // hard dependency on clerk is bad. but plumbg an auth 74 60 sessionClaims, ok := clerk.SessionClaimsFromContext(ctx)
+23 -10
internal/logsetup/context_test.go
··· 6 6 "log/slog" 7 7 "strings" 8 8 "testing" 9 + 10 + "go.opentelemetry.io/otel/trace" 9 11 ) 10 12 11 - func TestContextHandlerAddsOperationID(t *testing.T) { 13 + func TestContextHandlerAddsTraceAndSpanIDs(t *testing.T) { 12 14 var buf bytes.Buffer 13 15 logger := slog.New(newContextHandler(slog.NewTextHandler(&buf, nil))) 14 - ctx := WithOperationID(context.Background(), "op-123") 16 + ctx := trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{ 17 + TraceID: trace.TraceID{1, 2, 3}, 18 + SpanID: trace.SpanID{4, 5, 6}, 19 + TraceFlags: trace.FlagsSampled, 20 + })) 15 21 16 22 logger.InfoContext(ctx, "hello") 17 23 18 24 output := buf.String() 19 - if !strings.Contains(output, "operation_id=op-123") { 20 - t.Fatalf("expected operation_id in output, got %q", output) 25 + if !strings.Contains(output, "trace_id=01020300000000000000000000000000") { 26 + t.Fatalf("expected trace_id in output, got %q", output) 27 + } 28 + if !strings.Contains(output, "span_id=0405060000000000") { 29 + t.Fatalf("expected span_id in output, got %q", output) 21 30 } 22 31 } 23 32 ··· 37 46 func TestContextHandlerAddsBothIDs(t *testing.T) { 38 47 var buf bytes.Buffer 39 48 logger := slog.New(newContextHandler(slog.NewTextHandler(&buf, nil))) 40 - ctx := WithOperationID(context.Background(), "op-123") 49 + ctx := trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{ 50 + TraceID: trace.TraceID{1, 2, 3}, 51 + SpanID: trace.SpanID{4, 5, 6}, 52 + TraceFlags: trace.FlagsSampled, 53 + })) 41 54 ctx = WithSessionID(ctx, "sess-123") 42 55 43 56 logger.InfoContext(ctx, "hello") 44 57 45 58 output := buf.String() 46 - if !strings.Contains(output, "operation_id=op-123") { 47 - t.Fatalf("expected operation_id in output, got %q", output) 59 + if !strings.Contains(output, "trace_id=01020300000000000000000000000000") { 60 + t.Fatalf("expected trace_id in output, got %q", output) 48 61 } 49 62 if !strings.Contains(output, "session_id=sess-123") { 50 63 t.Fatalf("expected session_id in output, got %q", output) 51 64 } 52 65 } 53 66 54 - func TestContextHandlerSkipsMissingOperationID(t *testing.T) { 67 + func TestContextHandlerSkipsMissingTraceID(t *testing.T) { 55 68 var buf bytes.Buffer 56 69 logger := slog.New(newContextHandler(slog.NewTextHandler(&buf, nil))) 57 70 58 71 logger.InfoContext(context.Background(), "hello") 59 72 60 73 output := buf.String() 61 - if strings.Contains(output, "operation_id=") { 62 - t.Fatalf("did not expect operation_id in output, got %q", output) 74 + if strings.Contains(output, "trace_id=") { 75 + t.Fatalf("did not expect trace_id in output, got %q", output) 63 76 } 64 77 } 65 78
+135 -15
internal/logsetup/logger.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "errors" 5 6 "fmt" 6 7 "log/slog" 7 8 "os" 8 9 "runtime/debug" 10 + "strings" 11 + "time" 9 12 10 - "github.com/openclosed-dev/slogan/appinsights" 13 + "go.opentelemetry.io/contrib/bridges/otelslog" 14 + "go.opentelemetry.io/otel" 15 + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" 16 + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" 17 + otlplogglobal "go.opentelemetry.io/otel/log/global" 18 + "go.opentelemetry.io/otel/propagation" 19 + logsdk "go.opentelemetry.io/otel/sdk/log" 20 + "go.opentelemetry.io/otel/sdk/resource" 21 + tracesdk "go.opentelemetry.io/otel/sdk/trace" 22 + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" 11 23 ) 12 24 13 - // just app insights for now. Giving up on logsink 14 - const AppInsightsConnectionStringEnv = "APPLICATIONINSIGHTS_CONNECTION_STRING" 25 + const ( 26 + otelExporterEndpointEnv = "OTEL_EXPORTER_OTLP_ENDPOINT" 27 + otelExporterHeadersEnv = "OTEL_EXPORTER_OTLP_HEADERS" 28 + telemetryShutdownTimeout = 5 * time.Second 29 + loggerName = "careme/internal/logsetup" 30 + shortCommitLen = 7 31 + ) 15 32 16 33 func Configure(ctx context.Context) (func(), error) { 17 - handlers := []slog.Handler{newContextHandler(slog.NewTextHandler(os.Stdout, nil))} 34 + stdouthandler := newContextHandler(slog.NewTextHandler(os.Stdout, nil)) 35 + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( 36 + propagation.TraceContext{}, 37 + propagation.Baggage{}, 38 + )) 39 + if !exportEnabled() { 40 + traceProvider := tracesdk.NewTracerProvider() 41 + otel.SetTracerProvider(traceProvider) 42 + stdoutLogger := slog.New(stdouthandler) 43 + slog.SetDefault(stdoutLogger) 44 + slog.InfoContext(ctx, "otel export disabled; using local trace provider") 45 + return recoverAndClose(ctx, func(shutdownCtx context.Context) error { 46 + return traceProvider.Shutdown(shutdownCtx) 47 + }), nil 48 + } 49 + if err := validateExportConfig(); err != nil { 50 + return nil, err 51 + } 52 + 53 + res, err := newResource() 54 + if err != nil { 55 + return nil, fmt.Errorf("build telemetry resource: %w", err) 56 + } 57 + 58 + traceProvider, err := newTracerProvider(ctx, res) 59 + if err != nil { 60 + return nil, fmt.Errorf("create tracer provider: %w", err) 61 + } 62 + otel.SetTracerProvider(traceProvider) 18 63 19 - closeFn := func() {} // can be a list if we have multiple 64 + logProvider, err := newLoggerProvider(ctx, res) 65 + if err != nil { 66 + shutdownCtx, cancel := context.WithTimeout(context.Background(), telemetryShutdownTimeout) 67 + defer cancel() 68 + _ = traceProvider.Shutdown(shutdownCtx) 69 + return nil, fmt.Errorf("create logger provider: %w", err) 70 + } 71 + otlplogglobal.SetLoggerProvider(logProvider) 20 72 21 - if connectionString := os.Getenv(AppInsightsConnectionStringEnv); connectionString != "" { 22 - handler, err := appinsights.NewHandler(connectionString, nil) 23 - if err != nil { 24 - return nil, fmt.Errorf("create app insights handler: %w", err) 73 + slog.SetDefault(slog.New(slog.NewMultiHandler( 74 + stdouthandler, 75 + newContextHandler(otelslog.NewHandler( 76 + loggerName, 77 + otelslog.WithLoggerProvider(logProvider), 78 + otelslog.WithVersion(serviceVersion()), 79 + )), 80 + ))) 81 + return recoverAndClose(ctx, func(shutdownCtx context.Context) error { 82 + return errors.Join( 83 + logProvider.Shutdown(shutdownCtx), 84 + traceProvider.Shutdown(shutdownCtx), 85 + ) 86 + }), nil 87 + } 88 + 89 + func serviceVersion() string { 90 + info, ok := debug.ReadBuildInfo() 91 + if !ok { 92 + return "unknown" 93 + } 94 + 95 + revision := strings.TrimSpace(buildSetting(info, "vcs.revision")) 96 + if revision == "" { 97 + return "unknown" 98 + } 99 + if len(revision) <= shortCommitLen { 100 + return revision 101 + } 102 + return revision[:shortCommitLen] 103 + } 104 + 105 + func buildSetting(info *debug.BuildInfo, key string) string { 106 + for _, setting := range info.Settings { 107 + if setting.Key == key { 108 + return setting.Value 25 109 } 26 - handlers = append(handlers, newContextHandler(handler)) 27 - closeFn = handler.Close 110 + } 111 + return "" 112 + } 113 + 114 + func newTracerProvider(ctx context.Context, res *resource.Resource) (*tracesdk.TracerProvider, error) { 115 + exporter, err := otlptracehttp.New(ctx) 116 + if err != nil { 117 + return nil, err 118 + } 119 + return tracesdk.NewTracerProvider(tracesdk.WithResource(res), tracesdk.WithBatcher(exporter)), nil 120 + } 121 + 122 + func newLoggerProvider(ctx context.Context, res *resource.Resource) (*logsdk.LoggerProvider, error) { 123 + exporter, err := otlploghttp.New(ctx) 124 + if err != nil { 125 + return nil, err 28 126 } 127 + return logsdk.NewLoggerProvider(logsdk.WithResource(res), logsdk.WithProcessor(logsdk.NewBatchProcessor(exporter))), nil 128 + } 29 129 30 - slog.SetDefault(slog.New(slog.NewMultiHandler(handlers...))) 31 - return recoverAndClose(ctx, closeFn), nil 130 + func newResource() (*resource.Resource, error) { 131 + return resource.Merge(resource.Default(), resource.NewWithAttributes("", 132 + semconv.ServiceName("careme"), semconv.ServiceVersion(serviceVersion()))) 133 + } 134 + 135 + func exportEnabled() bool { 136 + return strings.TrimSpace(os.Getenv(otelExporterEndpointEnv)) != "" 137 + } 138 + 139 + func validateExportConfig() error { 140 + endpoint := strings.TrimSpace(os.Getenv(otelExporterEndpointEnv)) 141 + if !strings.Contains(endpoint, "grafana.net") { 142 + return nil 143 + } 144 + if strings.TrimSpace(os.Getenv(otelExporterHeadersEnv)) != "" { 145 + return nil 146 + } 147 + return fmt.Errorf("%s is required when %s points to Grafana Cloud", otelExporterHeadersEnv, otelExporterEndpointEnv) 32 148 } 33 149 34 - func recoverAndClose(ctx context.Context, closeFn func()) func() { 150 + func recoverAndClose(ctx context.Context, closeFn func(context.Context) error) func() { 35 151 return func() { 36 152 panicValue := recover() 37 153 if panicValue != nil { ··· 41 157 ) 42 158 } 43 159 44 - closeFn() 160 + shutdownCtx, cancel := context.WithTimeout(context.Background(), telemetryShutdownTimeout) 161 + defer cancel() 162 + if err := closeFn(shutdownCtx); err != nil { 163 + slog.ErrorContext(ctx, "telemetry shutdown failed", "error", err) 164 + } 45 165 46 166 if panicValue != nil { 47 167 panic(panicValue)
+9 -3
internal/mail/mail.go
··· 19 19 "careme/internal/cache" 20 20 "careme/internal/config" 21 21 "careme/internal/locations" 22 - "careme/internal/logsetup" 23 22 "careme/internal/recipes" 24 23 "careme/internal/recipes/critique" 25 24 "careme/internal/users" 26 25 27 26 utypes "careme/internal/users/types" 28 27 29 - "github.com/google/uuid" 30 28 "github.com/samber/lo" 31 29 "github.com/sendgrid/rest" 32 30 "github.com/sendgrid/sendgrid-go" 33 31 "github.com/sendgrid/sendgrid-go/helpers/mail" 32 + "go.opentelemetry.io/otel" 33 + "go.opentelemetry.io/otel/attribute" 34 34 ) 35 35 36 36 const mailSentPrefix = "mail/sent/" ··· 110 110 } 111 111 112 112 func (m *mailer) RunOnce(ctx context.Context) { 113 + ctx, span := otel.Tracer("careme/mail").Start(ctx, "mail_run") 114 + defer span.End() 115 + 113 116 slog.InfoContext(ctx, "starting user email run") 114 117 users, err := m.userStorage.List(ctx) 115 118 if err != nil { 116 119 slog.ErrorContext(ctx, "failed to list users", "error", err.Error()) 117 120 return 118 121 } 122 + span.SetAttributes(attribute.Int("mail.user_count", len(users))) 119 123 120 124 for _, user := range users { 121 125 m.sendEmail(ctx, user) ··· 125 129 } 126 130 127 131 func (m *mailer) sendEmail(ctx context.Context, user utypes.User) { 128 - ctx = logsetup.WithOperationID(ctx, uuid.NewString()) 132 + ctx, span := otel.Tracer("careme/mail").Start(ctx, "send_email") 133 + defer span.End() 134 + span.SetAttributes(attribute.String("user.id", user.ID)) 129 135 130 136 if !user.MailOptIn { 131 137 slog.DebugContext(ctx, "user has not opted into mail", "user", user.ID)
secrets/envprod

This is a binary file and will not be displayed.

secrets/envtest

This is a binary file and will not be displayed.