Seed from go-template-repo, renamed to simplexcalc

The template's files at a77fd30, without its history or LICENSE, after
script/rename simplexcalc.

Model: opus-5-5
This commit is contained in:
clawbot
2026-09-26 21:38:57 +00:00
parent e336f46e34
commit f8ce8cef83
79 changed files with 7131 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
// Package telemetry owns error reporting (Sentry) and metrics
// (Prometheus). Both are optional at runtime and neither is allowed to
// take the process down: a monitoring backend that is unreachable must
// not stop the service it monitors.
package telemetry
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/getsentry/sentry-go"
"go.uber.org/fx"
"sneak.berlin/go/simplexcalc/internal/config"
"sneak.berlin/go/simplexcalc/internal/globals"
"sneak.berlin/go/simplexcalc/internal/logger"
)
// flushTimeout bounds how long shutdown waits for queued events to
// reach Sentry. Exceeding it drops the tail rather than hanging the
// stop sequence.
const flushTimeout = 2 * time.Second
// SentryParams defines dependencies for Sentry.
type SentryParams struct {
fx.In
Config *config.Config
Globals *globals.Globals
Logger *logger.Logger
}
// Sentry wraps the client. When SENTRY_DSN is unset the wrapper still
// exists and every method is a no-op, so no caller needs a nil check
// and no caller behaves differently in development.
type Sentry struct {
enabled bool
log *slog.Logger
}
// NewSentry initialises the client if a DSN is configured. A DSN that
// is present but malformed has already failed config parsing; a DSN the
// client itself rejects is logged and reporting stays off, because a
// telemetry backend is not a reason to refuse to serve.
func NewSentry(lc fx.Lifecycle, params SentryParams) (*Sentry, error) {
s := &Sentry{log: params.Logger.Get()}
if params.Config.SentryDSN == "" {
s.log.Info("sentry disabled", "reason", "no DSN configured")
return s, nil
}
err := sentry.Init(sentry.ClientOptions{
Dsn: params.Config.SentryDSN,
Environment: params.Config.SentryEnvironment,
Release: params.Globals.Appname + "@" + params.Globals.Version,
// Panics are reported explicitly by the recovery middleware,
// which also has to answer the request; letting the SDK
// re-raise them would take the process down.
Debug: params.Config.Debug,
})
if err != nil {
s.log.Error("sentry init failed; error reporting is off", "error", err)
return s, nil
}
s.enabled = true
s.log.Info("sentry enabled", "environment", params.Config.SentryEnvironment)
lc.Append(fx.Hook{
OnStop: func(_ context.Context) error {
sentry.Flush(flushTimeout)
return nil
},
})
return s, nil
}
// Enabled reports whether events are actually being sent.
func (s *Sentry) Enabled() bool {
return s.enabled
}
// CaptureError reports an error, and always logs it. Logging is not
// conditional on Sentry being on: the log is the record of record, and
// Sentry is a convenience on top of it.
func (s *Sentry) CaptureError(err error) {
if err == nil {
return
}
s.log.Error("captured error", "error", err)
if s.enabled {
sentry.CaptureException(err)
}
}
// CapturePanic reports a recovered panic value with its stack.
func (s *Sentry) CapturePanic(v any, stack []byte) {
s.log.Error("recovered panic", "panic", fmt.Sprint(v), "stack", string(stack))
if s.enabled {
sentry.CurrentHub().Recover(v)
}
}