eeaad62b10
- internal/postgres: pgx-over-database/sql pool (otelsql), embedded goose
migrations into schema 'backend', committed go-jet code + cmd/jetgen tool.
- internal/account: durable accounts + unified telegram/email identities
(UUIDv7 keys), find-or-create provisioning with unique-conflict handling.
- internal/session: opaque 256-bit tokens stored as a SHA-256 hash, revoke-only
(no TTL); write-through cache gating /readyz; store + service.
- internal/telemetry: OTel tracer/meter providers (none/stdout) + request-timing
middleware; internal/config gains Postgres + OTel env loading.
- internal/server: /api/v1 {public,user,internal,admin} skeleton + X-User-ID
middleware; /readyz checks DB ping + cache; main wires
telemetry -> db+migrate -> warm cache -> server.
- Tests: unit + integration (build tag 'integration', testcontainers
postgres:17) for migrations, accounts, sessions, readyz; new integration.yaml.
- Docs: ARCHITECTURE, TESTING, PLAN refinements, root + backend READMEs.
Session/account REST handlers deferred to Stage 6 (gateway); OTLP + dashboards
to Stage 11.
75 lines
2.0 KiB
Go
75 lines
2.0 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// tracerName names the instrumentation scope for backend HTTP spans.
|
|
const tracerName = "scrabble/backend/server"
|
|
|
|
// Middleware returns gin middleware that, for every request, opens a server
|
|
// span, measures server-side latency, and emits a structured access log
|
|
// correlated with the active trace. It uses the globally-registered tracer, so
|
|
// spans are exported only when an exporter is configured, while the timing log
|
|
// is always emitted. Probe paths (/healthz, /readyz) log at debug level to keep
|
|
// the default log clean.
|
|
func Middleware(logger *zap.Logger) gin.HandlerFunc {
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
tracer := otel.Tracer(tracerName)
|
|
|
|
return func(c *gin.Context) {
|
|
start := time.Now()
|
|
route := c.FullPath()
|
|
if route == "" {
|
|
route = c.Request.URL.Path
|
|
}
|
|
|
|
ctx, span := tracer.Start(c.Request.Context(), c.Request.Method+" "+route)
|
|
c.Request = c.Request.WithContext(ctx)
|
|
|
|
c.Next()
|
|
|
|
status := c.Writer.Status()
|
|
elapsed := time.Since(start)
|
|
|
|
span.SetAttributes(
|
|
attribute.String("http.request.method", c.Request.Method),
|
|
attribute.String("http.route", route),
|
|
attribute.Int("http.response.status_code", status),
|
|
)
|
|
if status >= http.StatusInternalServerError {
|
|
span.SetStatus(codes.Error, http.StatusText(status))
|
|
}
|
|
span.End()
|
|
|
|
fields := []zap.Field{
|
|
zap.String("method", c.Request.Method),
|
|
zap.String("path", route),
|
|
zap.Int("status", status),
|
|
zap.Duration("latency", elapsed),
|
|
}
|
|
fields = append(fields, TraceFieldsFromContext(ctx)...)
|
|
|
|
if isProbePath(c.Request.URL.Path) {
|
|
logger.Debug("http request", fields...)
|
|
return
|
|
}
|
|
logger.Info("http request", fields...)
|
|
}
|
|
}
|
|
|
|
// isProbePath reports whether path is one of the unauthenticated infrastructure
|
|
// probes, whose access logs are demoted to debug level.
|
|
func isProbePath(path string) bool {
|
|
return path == "/healthz" || path == "/readyz"
|
|
}
|