package main import ( "context" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "otel-bi-analytics/internal/config" "otel-bi-analytics/internal/db" "otel-bi-analytics/internal/handler" "otel-bi-analytics/internal/scheduler" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/propagation" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.26.0" ) func main() { slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, }))) cfg := config.Load() ctx := context.Background() shutdown, err := setupOtel(ctx, cfg) if err != nil { slog.Error("failed to set up OTel", "err", err) os.Exit(1) } awDB, err := db.Open(ctx, cfg.AWConnStr, "aw") if err != nil { slog.Error("failed to connect to AW MSSQL", "err", err) os.Exit(1) } defer awDB.Close() wwiDB, err := db.Open(ctx, cfg.WWIConnStr, "wwi") if err != nil { slog.Error("failed to connect to WWI MSSQL", "err", err) os.Exit(1) } defer wwiDB.Close() pgPool, err := db.OpenPostgres(ctx, cfg.PostgresDSN) if err != nil { slog.Error("failed to connect to PostgreSQL", "err", err) os.Exit(1) } defer pgPool.Close() sched := scheduler.New(awDB, wwiDB, pgPool, cfg.DefaultTopN) sched.Start() defer sched.Stop() mux := http.NewServeMux() h := handler.New(awDB, wwiDB, pgPool, sched, cfg.DefaultTopN, cfg.ForecastHorizonDays, cfg.DefaultHistoryDays) h.RegisterRoutes(mux) srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.Port), Handler: otelhttp.NewHandler(mux, "analytics-service"), ReadTimeout: 60 * time.Second, WriteTimeout: 120 * time.Second, IdleTimeout: 120 * time.Second, } done := make(chan struct{}) go func() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT) <-quit slog.Info("shutting down") ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() _ = srv.Shutdown(ctx) shutdown(ctx) close(done) }() slog.Info("analytics service started", "port", cfg.Port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("server error", "err", err) os.Exit(1) } <-done slog.Info("shutdown complete") } func setupOtel(ctx context.Context, cfg config.Config) (func(context.Context), error) { res, err := resource.New(ctx, resource.WithAttributes( semconv.ServiceName(cfg.OtelServiceName), semconv.ServiceNamespace(cfg.OtelServiceNamespace), ), ) if err != nil { return nil, fmt.Errorf("create OTel resource: %w", err) } traceExporter, err := otlptracehttp.New(ctx, otlptracehttp.WithEndpointURL(cfg.OtelCollectorEndpoint+"/v1/traces"), otlptracehttp.WithInsecure(), ) if err != nil { return nil, fmt.Errorf("create OTLP trace exporter: %w", err) } tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(traceExporter), sdktrace.WithResource(res), ) otel.SetTracerProvider(tp) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, )) metricExporter, err := otlpmetrichttp.New(ctx, otlpmetrichttp.WithEndpointURL(cfg.OtelCollectorEndpoint+"/v1/metrics"), otlpmetrichttp.WithInsecure(), ) if err != nil { return nil, fmt.Errorf("create OTLP metric exporter: %w", err) } mp := sdkmetric.NewMeterProvider( sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter, sdkmetric.WithInterval(15*time.Second))), sdkmetric.WithResource(res), ) otel.SetMeterProvider(mp) return func(ctx context.Context) { if err := tp.Shutdown(ctx); err != nil { slog.Error("trace provider shutdown error", "err", err) } if err := mp.Shutdown(ctx); err != nil { slog.Error("metric provider shutdown error", "err", err) } }, nil }