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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user