Every log record now carries type ('http' for the request middleware
line, 'wrapper' for garmin execute() calls and forwarded wrapper.py
stderr, 'app' for everything else), class (Go type with package, or the
package/module for free functions), and method (the emitting Go/Python
function -- wrapper.py stamps it automatically, so its msg prefixes are
gone). msg is optional and omitted when empty; the redundant messages
('HTTP request', 'wrapper call', 'garmin wrapper stderr') are dropped,
the HTTP verb moves to http_method, source=garmin-wrapper is replaced by
type=wrapper, and the request_id middleware plumbing (applog
WithLogger/FromContext) is removed. applog.App(class, method) is the
tagging helper for app records.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
120 lines
3.4 KiB
Go
120 lines
3.4 KiB
Go
// Command geniusrund is geniusrun's backend server: syncs runs from Garmin,
|
|
// classifies them into workout kinds, and serves the REST API the frontend
|
|
// talks to.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"geniusrun/backend/internal/api"
|
|
"geniusrun/backend/internal/auth"
|
|
"geniusrun/backend/internal/config"
|
|
"geniusrun/backend/internal/garmin"
|
|
applog "geniusrun/backend/internal/log"
|
|
"geniusrun/backend/internal/store"
|
|
)
|
|
|
|
// fatal logs through the application JSON logger and exits -- the
|
|
// structured replacement for log.Fatalf, so even startup failures come
|
|
// out as JSON lines.
|
|
func fatal(msg string, err error) {
|
|
applog.App("main", "fatal").Error(msg, "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func main() {
|
|
// Bootstrap logger at info so failures loading the env config itself
|
|
// (which carries the real log level) are still emitted as JSON.
|
|
slog.SetDefault(applog.NewLogger("warn", os.Stdout))
|
|
|
|
envCfg, err := config.LoadEnv()
|
|
if err != nil {
|
|
fatal("load env config", err)
|
|
}
|
|
slog.SetDefault(applog.NewLogger(envCfg.LogLevel, os.Stdout))
|
|
|
|
db, err := store.Open(envCfg.DBPath)
|
|
if err != nil {
|
|
fatal("open database", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
values, err := db.ConfigValues(context.Background())
|
|
if err != nil {
|
|
fatal("read app config", err)
|
|
}
|
|
// Every registry key is mandatory in the DB: seed missing ones with
|
|
// their defaults so the config table is always fully populated (no
|
|
// code-side fallbacks anywhere downstream).
|
|
for _, k := range config.AppRegistry() {
|
|
if _, ok := values[k.Key]; !ok {
|
|
if err := db.SetConfigValue(context.Background(), k.Key, k.Default); err != nil {
|
|
fatal("seed app config default", err)
|
|
}
|
|
values[k.Key] = k.Default
|
|
}
|
|
}
|
|
appCfg, err := config.LoadApp(values)
|
|
if err != nil {
|
|
fatal("load app config", err)
|
|
}
|
|
|
|
authVerifier, err := auth.NewOIDCVerifier(context.Background(), auth.OIDCConfig{
|
|
IssuerURL: envCfg.OIDCIssuerURL,
|
|
ClientID: envCfg.OIDCClientID,
|
|
ClientSecret: envCfg.OIDCClientSecret,
|
|
RedirectURL: envCfg.OIDCRedirectURL,
|
|
RequiredRole: envCfg.OIDCRequiredRole,
|
|
})
|
|
if err != nil {
|
|
fatal("oidc verifier setup", err)
|
|
}
|
|
|
|
server := api.NewServer(
|
|
db,
|
|
garmin.NewClient,
|
|
garmin.ClientConfig{
|
|
PythonPath: envCfg.PythonPath,
|
|
TokenStorePath: envCfg.TokenStoreRoot,
|
|
},
|
|
garmin.SyncConfig{},
|
|
authVerifier,
|
|
api.SessionConfig{
|
|
Secret: envCfg.SessionSecret,
|
|
Duration: appCfg.SessionDuration,
|
|
SetupTimeout: appCfg.SetupTimeout,
|
|
Secure: envCfg.SessionSecure,
|
|
BackendURL: envCfg.BackendURL,
|
|
FrontendURL: envCfg.FrontendURL,
|
|
})
|
|
|
|
for _, e := range envCfg.DisplayEnv() {
|
|
server.EnvVars = append(server.EnvVars, api.EnvVar{Name: e.Name, Value: e.Value})
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
httpServer := &http.Server{Addr: envCfg.BackendAddr, Handler: server.Router()}
|
|
go func() {
|
|
applog.App("main", "main").Info("geniusrund listening", "addr", envCfg.BackendAddr)
|
|
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
fatal("http server", err)
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
applog.App("main", "main").Info("shutting down")
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
|
applog.App("main", "main").Error("http server shutdown", "error", err)
|
|
}
|
|
}
|