Openstatus
www.openstatus.dev
1package server
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "net/http"
8 "os"
9 "strconv"
10 "time"
11
12 "github.com/jmoiron/sqlx"
13 _ "github.com/joho/godotenv/autoload"
14 "go.opentelemetry.io/contrib/bridges/otelslog"
15 "go.opentelemetry.io/otel/attribute"
16 "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
17 "go.opentelemetry.io/otel/log/global"
18 sdklog "go.opentelemetry.io/otel/sdk/log"
19 "go.opentelemetry.io/otel/sdk/resource"
20 semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
21
22 "github.com/openstatushq/openstatus/apps/private-location/internal/database"
23)
24
25type Server struct {
26 port int
27 db *sqlx.DB
28 logger *slog.Logger
29 logProvider *sdklog.LoggerProvider
30}
31
32// NewServer returns an HTTP server and a cleanup function to shutdown the log provider.
33func NewServer() (*http.Server, func(context.Context)) {
34 portStr := os.Getenv("PORT")
35 if portStr == "" {
36 portStr = "8080"
37 }
38 port, err := strconv.Atoi(portStr)
39 if err != nil {
40 fmt.Fprintf(os.Stderr, "invalid PORT value %q: %v\n", portStr, err)
41 os.Exit(1)
42 }
43
44 logger, logProvider := setupLogger()
45
46 newServer := &Server{
47 port: port,
48 db: database.New(),
49 logger: logger,
50 logProvider: logProvider,
51 }
52
53 // Declare Server config
54 server := &http.Server{
55 Addr: fmt.Sprintf(":%d", newServer.port),
56 Handler: newServer.RegisterRoutes(),
57 IdleTimeout: time.Minute,
58 ReadTimeout: 10 * time.Second,
59 WriteTimeout: 30 * time.Second,
60 }
61
62 // Return cleanup function for graceful shutdown
63 cleanup := func(ctx context.Context) {
64 if logProvider != nil {
65 logProvider.Shutdown(ctx)
66 }
67 database.Close()
68 }
69
70 return server, cleanup
71}
72
73func setupLogger() (*slog.Logger, *sdklog.LoggerProvider) {
74 ctx := context.Background()
75
76 axiomToken := env("AXIOM_TOKEN", "")
77 axiomDataset := env("AXIOM_DATASET", "dev")
78
79 // If no Axiom token, return a standard logger
80 if axiomToken == "" {
81 logger := slog.Default()
82 return logger, nil
83 }
84
85 res := resource.NewWithAttributes(
86 semconv.SchemaURL,
87 semconv.ServiceNameKey.String("openstatus-private-location"),
88 semconv.ServiceVersionKey.String("1.0.0"),
89 attribute.String("environment", "production"),
90 )
91
92 // Set up OTLP log exporter for Axiom
93 exporter, err := otlploghttp.New(ctx,
94 otlploghttp.WithEndpointURL("https://eu-central-1.aws.edge.axiom.co/v1/logs"),
95 otlploghttp.WithHeaders(map[string]string{
96 "Authorization": "Bearer " + axiomToken,
97 "X-Axiom-Dataset": axiomDataset,
98 }),
99 )
100 if err != nil {
101 fmt.Fprintf(os.Stderr, "failed to create OTLP exporter: %v\n", err)
102 return slog.Default(), nil
103 }
104
105 // Create log provider with resource and batch processor
106 logProvider := sdklog.NewLoggerProvider(
107 sdklog.WithResource(res),
108 sdklog.WithProcessor(sdklog.NewBatchProcessor(exporter)),
109 )
110
111 global.SetLoggerProvider(logProvider)
112 logger := otelslog.NewLogger("openstatus-private-location")
113 slog.SetDefault(logger)
114
115 return logger, logProvider
116}
117
118func env(key, fallback string) string {
119 if value, ok := os.LookupEnv(key); ok {
120 return value
121 }
122 return fallback
123}