observability.go 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. // Package observability is the shared logging + metrics + OTel stub.
  2. //
  3. // M0: slog (stdlib structured logging, JSON output) and Prometheus
  4. // metrics via promhttp. OTel is a stub — we wire it in M9.
  5. //
  6. // All services should call Init() in main and then use the returned
  7. // logger and the package-level metrics.
  8. package observability
  9. import (
  10. "context"
  11. "log/slog"
  12. "net/http"
  13. "os"
  14. "strings"
  15. "github.com/prometheus/client_golang/prometheus"
  16. "github.com/prometheus/client_golang/prometheus/promhttp"
  17. )
  18. // Init returns a slog.Logger configured for the env. JSON in non-dev
  19. // for log shippers, text in dev for human eyes.
  20. func Init(env, level, serviceName string) *slog.Logger {
  21. lvl := parseLevel(level)
  22. opts := &slog.HandlerOptions{Level: lvl}
  23. var h slog.Handler
  24. if env == "dev" {
  25. h = slog.NewTextHandler(os.Stdout, opts)
  26. } else {
  27. h = slog.NewJSONHandler(os.Stdout, opts)
  28. }
  29. logger := slog.New(h).With(
  30. slog.String("service", serviceName),
  31. slog.String("env", env),
  32. )
  33. slog.SetDefault(logger)
  34. return logger
  35. }
  36. func parseLevel(s string) slog.Level {
  37. switch strings.ToLower(s) {
  38. case "debug":
  39. return slog.LevelDebug
  40. case "warn":
  41. return slog.LevelWarn
  42. case "error":
  43. return slog.LevelError
  44. default:
  45. return slog.LevelInfo
  46. }
  47. }
  48. // MetricsHandler returns an http.Handler that serves Prometheus
  49. // metrics from the given registry.
  50. func MetricsHandler(reg *prometheus.Registry) http.Handler {
  51. return promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg})
  52. }
  53. // HealthHandler is the standard /health endpoint. Liveness only —
  54. // readiness (can serve traffic) is a separate concern, added per
  55. // service as it comes online.
  56. func HealthHandler(serviceName string) http.Handler {
  57. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  58. w.Header().Set("Content-Type", "application/json")
  59. _, _ = w.Write([]byte(`{"status":"ok","service":"` + serviceName + `"}`))
  60. })
  61. }
  62. // CtxWithTrace is a stub for M9 when OTel is wired in. For now
  63. // it just attaches the request id (or generates one) so logs can
  64. // be correlated.
  65. func CtxWithTrace(ctx context.Context, traceID string) context.Context {
  66. if traceID == "" {
  67. // tiny random — not crypto-strong, but unique enough
  68. traceID = newTraceID()
  69. }
  70. return context.WithValue(ctx, traceKey{}, traceID)
  71. }
  72. // TraceFromCtx returns the trace id, or "" if none.
  73. func TraceFromCtx(ctx context.Context) string {
  74. v, _ := ctx.Value(traceKey{}).(string)
  75. return v
  76. }
  77. type traceKey struct{}
  78. func newTraceID() string {
  79. b := make([]byte, 8)
  80. if _, err := readRandom(b); err != nil {
  81. return "0"
  82. }
  83. return hexEncode(b)
  84. }