// This template contains a simple // app with OTEL bootstrap that will create an // HTTP server configured by environment that exports // spans and metrics to an OTEL collector if configured // to do so. Will also stand up a prometheus metrics // endpoint. // // Configuration and logger stored in context // Reference implementation of the provided packages package main import ( "context" "net/http" "os" "os/signal" "sync" "time" "github.com/rs/zerolog" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" "golang.org/x/sys/unix" "gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/config" "gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/logging" "gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/otel" "gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/srv" ) var ( cfg *config.AppConfig l *zerolog.Logger tracer trace.Tracer ) func main() { ctx, cncl := signal.NotifyContext(context.Background(), os.Interrupt, unix.SIGTERM) defer cncl() shutdownFuncs := make([]func(context.Context) error, 0) // Load configuration and setup logging ctx = setupConfigAndLogging(ctx) // Set up OTEL ctx, otelShutdown := otel.Init(ctx) shutdownFuncs = append(shutdownFuncs, otelShutdown) tracer = otel.MustTracerFromCtx(ctx) // Start App ctx, initSpan := tracer.Start(ctx, "init") // Start HTTP Server dummyFuncs := []srv.HTTPFunc{ { Path: "/dummy", HandlerFunc: func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello world")) }, }, } httpShutdown, httpDone := srv.MustInitHTTPServer(ctx, dummyFuncs...) shutdownFuncs = append(shutdownFuncs, httpShutdown) // Startup Complete l.Info(). Str("name", cfg.Name). Str("version", cfg.Version). Str("logLevel", cfg.Logging.Level). Msg("app initialized") initSpan.SetStatus(codes.Ok, "") initSpan.End() // Wait for signal select { case <-httpDone: l.Warn().Msg("shutting down early on http server done") case <-ctx.Done(): l.Warn().Str("reason", ctx.Err().Error()). Msg("shutting down on context done") } shutdown(shutdownFuncs...) } func shutdown(shutdownFuncs ...func(context.Context) error) { now := time.Now() doneCtx, cncl := context.WithTimeout(context.Background(), 15*time.Second) defer func() { if doneCtx.Err() == context.DeadlineExceeded { l.Err(doneCtx.Err()). Dur("shutdownTime", time.Since(now)). Msg("app shutdown aborted") } else { l.Info(). Int("shutdownFuncsCalled", len(shutdownFuncs)). Dur("shutdownTime", time.Since(now)). Msg("app shutdown normally") } cncl() }() doneCtx, span := tracer.Start(doneCtx, "shutdown") defer span.End() var wg sync.WaitGroup wg.Add(len(shutdownFuncs)) for _, f := range shutdownFuncs { go func() { defer wg.Done() err := f(doneCtx) if err != nil { span.SetStatus(codes.Error, "shutdown failed") span.RecordError(err) l.Err(err).Send() } }() } wg.Wait() } func setupConfigAndLogging(ctx context.Context) context.Context { ctx, err := config.LoadConfig(ctx) if err != nil { panic(err) } cfg = config.MustFromCtx(ctx) ctx = logging.MustInitLogging(ctx) l = zerolog.Ctx(ctx) l.Trace().Any("config", *cfg).Send() return ctx }