Files
galaxy-game/mail/internal/api/internalhttp/observability.go
T
2026-04-17 18:39:16 +02:00

115 lines
2.7 KiB
Go

package internalhttp
import (
"log/slog"
"net/http"
"time"
"galaxy/mail/internal/logging"
"galaxy/mail/internal/telemetry"
"go.opentelemetry.io/otel/attribute"
)
type edgeOutcome string
const (
edgeOutcomeSuccess edgeOutcome = "success"
edgeOutcomeRejected edgeOutcome = "rejected"
edgeOutcomeFailed edgeOutcome = "failed"
)
func instrumentRoute(route string, logger *slog.Logger, telemetryRuntime *telemetry.Runtime, next http.Handler) http.Handler {
if logger == nil {
logger = slog.Default()
}
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
startedAt := time.Now()
recorder := &observedResponseWriter{
ResponseWriter: writer,
statusCode: http.StatusOK,
}
next.ServeHTTP(recorder, request)
duration := time.Since(startedAt)
outcome := outcomeFromStatusCode(recorder.statusCode)
attrs := []attribute.KeyValue{
attribute.String("route", route),
attribute.String("method", request.Method),
attribute.String("edge_outcome", string(outcome)),
}
if recorder.errorCode != "" {
attrs = append(attrs, attribute.String("error_code", recorder.errorCode))
}
if telemetryRuntime != nil {
telemetryRuntime.RecordInternalHTTPRequest(request.Context(), attrs, duration)
}
logArgs := []any{
"component", "internal_http",
"transport", "http",
"route", route,
"method", request.Method,
"status_code", recorder.statusCode,
"duration_ms", float64(duration.Microseconds()) / 1000,
"edge_outcome", string(outcome),
}
if recorder.errorCode != "" {
logArgs = append(logArgs, "error_code", recorder.errorCode)
}
logArgs = append(logArgs, logging.TraceAttrsFromContext(request.Context())...)
switch outcome {
case edgeOutcomeSuccess:
logger.Info("internal request completed", logArgs...)
case edgeOutcomeFailed:
logger.Error("internal request failed", logArgs...)
default:
logger.Warn("internal request rejected", logArgs...)
}
})
}
type observedResponseWriter struct {
http.ResponseWriter
statusCode int
errorCode string
wroteHeader bool
}
func (writer *observedResponseWriter) WriteHeader(statusCode int) {
if writer.wroteHeader {
return
}
writer.statusCode = statusCode
writer.wroteHeader = true
writer.ResponseWriter.WriteHeader(statusCode)
}
func (writer *observedResponseWriter) Write(payload []byte) (int, error) {
if !writer.wroteHeader {
writer.WriteHeader(http.StatusOK)
}
return writer.ResponseWriter.Write(payload)
}
func (writer *observedResponseWriter) SetErrorCode(code string) {
writer.errorCode = code
}
func outcomeFromStatusCode(statusCode int) edgeOutcome {
switch {
case statusCode >= 500:
return edgeOutcomeFailed
case statusCode >= 400:
return edgeOutcomeRejected
default:
return edgeOutcomeSuccess
}
}