Stage 1: backend foundation (Postgres, sessions, accounts, OTel)
- 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.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user