55 "encoding/hex"
66 "errors"
77 "fmt"
8- "log"
8+ "log/slog "
99 "net/http"
1010 "net/url"
1111 "os"
@@ -19,12 +19,17 @@ import (
1919 "github.com/ianlancetaylor/jsonschema"
2020 "github.com/spf13/cobra"
2121 "github.com/spf13/viper"
22+ slogctx "github.com/veqryn/slog-context"
2223 oasapi "github.com/zitadel/nextgen/api/generated"
2324 "github.com/zitadel/nextgen/internal/api"
25+ "github.com/zitadel/nextgen/internal/api/middleware"
2426 "github.com/zitadel/nextgen/internal/bootstrap/users"
2527 "github.com/zitadel/nextgen/internal/crypto"
2628 "github.com/zitadel/nextgen/internal/domain"
2729 "github.com/zitadel/nextgen/internal/domain/idgen"
30+ "github.com/zitadel/nextgen/internal/instrumentation"
31+ "github.com/zitadel/nextgen/internal/instrumentation/zlog"
32+ "github.com/zitadel/nextgen/internal/instrumentation/zotel"
2833 "github.com/zitadel/nextgen/internal/secrets"
2934 "github.com/zitadel/nextgen/internal/service"
3035 "github.com/zitadel/nextgen/internal/staticui/console"
@@ -34,6 +39,8 @@ import (
3439 "github.com/zitadel/nextgen/internal/storage/database/dialect/postgres/embedded"
3540 "github.com/zitadel/nextgen/internal/storage/database/repository"
3641 "github.com/zitadel/oidc/v3/pkg/op"
42+ "go.opentelemetry.io/contrib/bridges/otelslog"
43+ "go.opentelemetry.io/otel/log"
3744)
3845
3946func NewCommand () * cobra.Command {
@@ -48,13 +55,7 @@ func NewCommand() *cobra.Command {
4855 if err != nil {
4956 return err
5057 }
51-
52- pool , err := startDatabase (cmd .Context (), cfg )
53- if err != nil {
54- return err
55- }
56-
57- return run (cmd .Context (), cfg , pool , userFiles )
58+ return run (cmd .Context (), cfg , userFiles )
5859 },
5960 }
6061
@@ -64,61 +65,61 @@ func NewCommand() *cobra.Command {
6465 return cmd
6566}
6667
67- func startDatabase (ctx context.Context , cfg Config ) (database.Pool , error ) {
68- connector , err := buildDatabaseConnector (cfg )
69- if err != nil {
70- return nil , err
71- }
72- pool , err := connector .Connect (ctx )
73- if err != nil {
74- return nil , err
75- }
76- err = pool .Migrate (ctx )
68+ func run (ctx context.Context , cfg Config , userFiles []string ) error {
69+ var err error
70+ sfs := & ShutdownFuncs {}
71+ defer func () {
72+ if err != nil {
73+ slog .Error ("run error" , slogctx .Err (err ))
74+ }
75+ err = sfs .Exec (context .WithoutCancel (ctx ))
76+ if err != nil {
77+ slog .Error ("shutdown error" , slogctx .Err (err ))
78+ os .Exit (1 )
79+ return
80+ }
81+ slog .Info ("shut down application" )
82+ }()
83+
84+ slog .Info ("building server" )
85+
86+ metrics , err := zotel .NewOtelMetrics (ctx , zotel.MetricsConfig {
87+ ServiceName : cfg .Instrumentation .ServiceName ,
88+ TraceIdFraction : cfg .Instrumentation .Trace .Fraction ,
89+ TraceExporter : cfg .Instrumentation .Trace .Exporter ,
90+ MetricExporter : cfg .Instrumentation .Metric .Exporter ,
91+ LogExporter : cfg .Instrumentation .Log .Exporter ,
92+ })
7793 if err != nil {
78- return nil , err
94+ return fmt . Errorf ( "failed to create otel metrics: %w" , err )
7995 }
80- return pool , nil
81- }
96+ sfs .Add (metrics .Shutdown )
8297
83- func buildDatabaseConnector (cfg Config ) (database.Connector , error ) {
84- if len (cfg .Database .Raw ) == 0 {
85- options := embeddedPostgresOptions (cfg .Server .DataDir )
86- log .Printf ("no database dialect configured, starting embedded postgres in %s" , filepath .Dir (options .DataPath ))
87- return embedded .NewConnector (options ), nil
88- }
89- return cfg .Database .Build ()
90- }
98+ setUpLogging (cfg .Instrumentation .Log , metrics .LoggerProvider ())
9199
92- func embeddedPostgresOptions (dataDir string ) embedded.Options {
93- root := filepath .Join (dataDir , "embedded-postgres" )
94- return embedded.Options {
95- RuntimePath : filepath .Join (root , "runtime" ),
96- DataPath : filepath .Join (root , "data" ),
97- CachePath : filepath .Join (root , "cache" ),
98- LogPath : filepath .Join (root , "postgres.log" ),
99- Logger : os .Stdout ,
100+ pool , err := startDatabase (ctx , cfg )
101+ if err != nil {
102+ return err
100103 }
101- }
102-
103- func run (ctx context.Context , cfg Config , pool database.Pool , userFiles []string ) error {
104- defer func () {
105- if err := pool .Close (context .Background ()); err != nil {
106- log .Printf ("close database pool: %v" , err )
104+ sfs .Add (func (ctx context.Context ) error {
105+ if err := pool .Close (ctx ); err != nil {
106+ return fmt .Errorf ("failed close database pool: %w" , err )
107107 }
108- }()
108+ return nil
109+ })
109110
110111 crypter , err := buildCrypter (cfg .Server .EncryptionKey )
111112 if err != nil {
112- return err
113+ return fmt . Errorf ( "failed to create Crypter: %w" , err )
113114 }
114115
115116 passwordHasher , err := cfg .PasswordHasher .NewHasher ()
116117 if err != nil {
117- return fmt .Errorf ("build password hasher: %w" , err )
118+ return fmt .Errorf ("failed to build password hasher: %w" , err )
118119 }
119120
120121 if err := users .Import (ctx , pool , passwordHasher , users .DialectFromConfig (cfg .Database .Raw ), userFiles ); err != nil {
121- return fmt .Errorf ("bootstrap users: %w" , err )
122+ return fmt .Errorf ("failed to bootstrap users: %w" , err )
122123 }
123124
124125 // ── Repositories ─────────────────
@@ -136,22 +137,23 @@ func run(ctx context.Context, cfg Config, pool database.Pool, userFiles []string
136137 // ── Schema Stuff ─────────────────
137138 schemaCache , err := lru.New2Q [string , * jsonschema.Schema ](cfg .Schema .LRUCacheSize )
138139 if err != nil {
139- return fmt .Errorf ("build schema cache: %w" , err )
140+ return fmt .Errorf ("failed to build schema cache: %w" , err )
140141 }
142+
141143 var builtinPublicBase * url.URL
142144 if cfg .Schema .BuiltinPublicBase != "" {
143145 builtinPublicBase , err = url .Parse (cfg .Schema .BuiltinPublicBase )
144146 if err != nil {
145- return fmt .Errorf ("parse builtin public base: %w" , err )
147+ return fmt .Errorf ("failed to parse builtin public base: %w" , err )
146148 }
147149 }
148- schemaResolverWithHTTP := domain .NewJSONSchemaResolver (schemaRepo , schemaCache , 10 , 1000_000 , & http.Client {}, builtinPublicBase )
149150
151+ schemaResolverWithHTTP := domain .NewJSONSchemaResolver (schemaRepo , schemaCache , 10 , 1000_000 , & http.Client {}, builtinPublicBase )
150152 // storageSchemaResolver without an HTTP client to fetch tenant schemas from the cache/storage
151153 storageSchemaResolver := domain .NewJSONSchemaResolver (schemaRepo , schemaCache , 10 , 1000_000 , nil , builtinPublicBase )
152154 schemaValidator , err := domain .NewSchemaValidator (builtinPublicBase .String ())
153155 if err != nil {
154- return fmt .Errorf ("build schema validator: %w" , err )
156+ return fmt .Errorf ("failed to build schema validator: %w" , err )
155157 }
156158
157159 // ── Services ─────────────────────
@@ -217,14 +219,20 @@ func run(ctx context.Context, cfg Config, pool database.Pool, userFiles []string
217219 flowDefinitionSvc ,
218220 teamService ),
219221 api .NewSecurityHandler (),
222+ oasapi .WithMiddleware (
223+ middleware .AddOperationIdToContext (),
224+ // logging is done at net/http level
225+ ),
226+ oasapi .WithMeterProvider (metrics .MeterProvider ()),
227+ oasapi .WithTracerProvider (metrics .TracerProvider ()),
220228 oasapi .WithErrorHandler (api .OgenErrorHandler ))
221229 if err != nil {
222- return fmt .Errorf ("build api server: %w" , err )
230+ return fmt .Errorf ("failed to build api server: %w" , err )
223231 }
224232
225- mux , err := buildHTTPMux (cfg .Server , oasServer )
233+ mux , err := buildHTTPMux (cfg .Server , idgen . NewULID (), oasServer )
226234 if err != nil {
227- return err
235+ return fmt . Errorf ( "failed to build http mux: %w" , err )
228236 }
229237
230238 httpServer := & http.Server {
@@ -238,10 +246,11 @@ func run(ctx context.Context, cfg Config, pool database.Pool, userFiles []string
238246
239247 serverErr := make (chan error , 1 )
240248 go func () {
241- log . Printf ("server listening on %s " , httpServer .Addr )
249+ slog . Info ("server listening for requests " , slog . String ( "address" , httpServer .Addr ) )
242250 if err := httpServer .ListenAndServe (); err != nil && ! errors .Is (err , http .ErrServerClosed ) {
243251 serverErr <- err
244252 }
253+ slog .Debug ("stopped listening" )
245254 close (serverErr )
246255 }()
247256
@@ -252,12 +261,15 @@ func run(ctx context.Context, cfg Config, pool database.Pool, userFiles []string
252261 if err := httpServer .Shutdown (shutdownCtx ); err != nil {
253262 return fmt .Errorf ("shutdown server: %w" , err )
254263 }
264+ slog .Info ("server shut down" )
255265 return nil
256266 case err := <- serverErr :
257267 return err
258268 }
259269}
260270
271+ // ----------------------------- CONFIG --------------------------------------
272+
261273func loadConfig (configPath string ) (Config , error ) {
262274 v := viper .NewWithOptions (viper .ExperimentalBindStruct ())
263275 v .SetEnvPrefix ("NEXTGEN" )
@@ -283,6 +295,20 @@ func loadConfig(configPath string) (Config, error) {
283295 v .SetDefault ("schema.builtin_public_base" , "https://nextgen.com/api/schemas" ) // todo: temp, review
284296 v .SetDefault ("session.default_ttl" , domain .SessionAnonymousTTL )
285297 v .SetDefault ("session.max_ttl" , 720 * time .Hour )
298+ v .SetDefault ("instrumentation.service_name" , "Zitadel" )
299+ v .SetDefault ("instrumentation.log.level" , zlog .LevelInfo )
300+ v .SetDefault ("instrumentation.log.streams" , []zlog.Stream {
301+ zlog .StreamRuntime ,
302+ zlog .StreamReady ,
303+ zlog .StreamRequest ,
304+ zlog .StreamService ,
305+ zlog .StreamStorage ,
306+ })
307+ v .SetDefault ("instrumentation.log.format" , instrumentation .LogFormatText )
308+ v .SetDefault ("instrumentation.log.add_source" , true )
309+ v .SetDefault ("instrumentation.log.errors.report_location" , true )
310+ v .SetDefault ("instrumentation.log.errors.stack_trace" , true )
311+ v .SetDefault ("instrumentation.trace.fraction" , 1.0 )
286312
287313 // AutomaticEnv only resolves nested keys viper already knows about
288314 // (via default, config file, fields of config struct or explicit BindEnv).
@@ -327,7 +353,9 @@ func mustBindEnv(v *viper.Viper, key string) {
327353 }
328354}
329355
330- func buildHTTPMux (cfg ServerConfig , apiHandler http.Handler ) (* http.ServeMux , error ) {
356+ // ----------------------------- HTTP --------------------------------------
357+
358+ func buildHTTPMux (cfg ServerConfig , reqIdGen idgen.Generator , apiHandler http.Handler ) (* http.ServeMux , error ) {
331359 mux := http .NewServeMux ()
332360
333361 if cfg .LoginEnabled {
@@ -354,10 +382,56 @@ func buildHTTPMux(cfg ServerConfig, apiHandler http.Handler) (*http.ServeMux, er
354382 mux .Handle (cfg .ConsolePath + "/" , consoleHandler )
355383 }
356384
357- mux .Handle ("/" , api .WithRequestHostMiddleware (apiHandler ))
385+ mux .Handle ("/" ,
386+ middleware .WithRequestIdentification (reqIdGen ,
387+ middleware .WithLogging (
388+ api .WithRequestHostMiddleware (apiHandler ),
389+ ),
390+ ),
391+ )
358392 return mux , nil
359393}
360394
395+ // ----------------------------- STORAGE --------------------------------------
396+
397+ func startDatabase (ctx context.Context , cfg Config ) (database.Pool , error ) {
398+ connector , err := buildDatabaseConnector (cfg )
399+ if err != nil {
400+ return nil , err
401+ }
402+ pool , err := connector .Connect (ctx )
403+ if err != nil {
404+ return nil , err
405+ }
406+ err = pool .Migrate (ctx )
407+ if err != nil {
408+ return nil , err
409+ }
410+ return pool , nil
411+ }
412+
413+ func buildDatabaseConnector (cfg Config ) (database.Connector , error ) {
414+ if len (cfg .Database .Raw ) == 0 {
415+ options := embeddedPostgresOptions (cfg .Server .DataDir )
416+ slog .Info ("no database dialect configured, starting embedded postgres" , slog .String ("filePath" , filepath .Dir (options .DataPath )))
417+ return embedded .NewConnector (options ), nil
418+ }
419+ return cfg .Database .Build ()
420+ }
421+
422+ func embeddedPostgresOptions (dataDir string ) embedded.Options {
423+ root := filepath .Join (dataDir , "embedded-postgres" )
424+ return embedded.Options {
425+ RuntimePath : filepath .Join (root , "runtime" ),
426+ DataPath : filepath .Join (root , "data" ),
427+ CachePath : filepath .Join (root , "cache" ),
428+ LogPath : filepath .Join (root , "postgres.log" ),
429+ Logger : os .Stdout ,
430+ }
431+ }
432+
433+ // ----------------------------- CRYPTO --------------------------------------
434+
361435// buildCrypter decodes a hex-encoded crypter key and constructs a
362436// [crypto.Crypter]. The key must decode to exactly 32 bytes;
363437// anything else is a configuration error.
@@ -375,3 +449,25 @@ func buildCrypter(hexKey string) (crypto.Crypter, error) {
375449 crypter := op .NewAES256GCMCrypto ([32 ]byte (key ), "" )
376450 return crypter , nil
377451}
452+
453+ // ----------------------------- INSTRUMENTATION --------------------------------------
454+
455+ func setUpLogging (cfg instrumentation.LogConfig , otelProvider log.LoggerProvider ) {
456+ otelHandler := otelslog .NewHandler (
457+ Name ,
458+ otelslog .WithLoggerProvider (otelProvider ),
459+ )
460+
461+ stdErrHandler := cfg .Format .ErrorHandler (cfg .SlogHandlerOptions ())
462+ handler := zlog .NewHandler (
463+ cfg .Level ,
464+ cfg .Streams ,
465+ slog .NewMultiHandler (
466+ otelHandler ,
467+ stdErrHandler ,
468+ ),
469+ )
470+ logger := zlog .NewLogger (handler )
471+ logger .Info ("structured logger configured" , "config_level" , cfg .Level , "format" , cfg .Format )
472+ slog .SetDefault (logger )
473+ }
0 commit comments