Add OpenTelemetry instrumentation with middleware-only approach
This commit is contained in:
@@ -11,16 +11,41 @@ import (
|
||||
|
||||
// Config represents the application configuration
|
||||
type Config struct {
|
||||
Server struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
}
|
||||
Shutdown struct {
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
}
|
||||
Logging struct {
|
||||
JSON bool `mapstructure:"json"`
|
||||
}
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Shutdown ShutdownConfig `mapstructure:"shutdown"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
Telemetry TelemetryConfig `mapstructure:"telemetry"`
|
||||
}
|
||||
|
||||
// ServerConfig holds server-related configuration
|
||||
type ServerConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
}
|
||||
|
||||
// ShutdownConfig holds shutdown-related configuration
|
||||
type ShutdownConfig struct {
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
}
|
||||
|
||||
// LoggingConfig holds logging-related configuration
|
||||
type LoggingConfig struct {
|
||||
JSON bool `mapstructure:"json"`
|
||||
}
|
||||
|
||||
// TelemetryConfig holds OpenTelemetry-related configuration
|
||||
type TelemetryConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
OTLPEndpoint string `mapstructure:"otlp_endpoint"`
|
||||
ServiceName string `mapstructure:"service_name"`
|
||||
Insecure bool `mapstructure:"insecure"`
|
||||
Sampler SamplerConfig `mapstructure:"sampler"`
|
||||
}
|
||||
|
||||
// SamplerConfig holds tracing sampler configuration
|
||||
type SamplerConfig struct {
|
||||
Type string `mapstructure:"type"`
|
||||
Ratio float64 `mapstructure:"ratio"`
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from file, environment variables, and defaults
|
||||
@@ -28,13 +53,21 @@ type Config struct {
|
||||
// To specify a custom config file path, set DLC_CONFIG_FILE environment variable
|
||||
func LoadConfig() (*Config, error) {
|
||||
v := viper.New()
|
||||
|
||||
|
||||
// Set default values
|
||||
v.SetDefault("server.host", "0.0.0.0")
|
||||
v.SetDefault("server.port", 8080)
|
||||
v.SetDefault("shutdown.timeout", 30*time.Second)
|
||||
v.SetDefault("logging.json", false)
|
||||
|
||||
|
||||
// Telemetry defaults
|
||||
v.SetDefault("telemetry.enabled", false)
|
||||
v.SetDefault("telemetry.otlp_endpoint", "localhost:4317")
|
||||
v.SetDefault("telemetry.service_name", "DanceLessonsCoach")
|
||||
v.SetDefault("telemetry.insecure", true)
|
||||
v.SetDefault("telemetry.sampler.type", "parentbased_always_on")
|
||||
v.SetDefault("telemetry.sampler.ratio", 1.0)
|
||||
|
||||
// Check for custom config file path via environment variable
|
||||
if configFile := os.Getenv("DLC_CONFIG_FILE"); configFile != "" {
|
||||
v.SetConfigFile(configFile)
|
||||
@@ -45,7 +78,7 @@ func LoadConfig() (*Config, error) {
|
||||
v.SetConfigType("yaml")
|
||||
v.AddConfigPath(".")
|
||||
}
|
||||
|
||||
|
||||
// Read config file if it exists
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
@@ -56,7 +89,7 @@ func LoadConfig() (*Config, error) {
|
||||
} else {
|
||||
log.Info().Str("config_file", v.ConfigFileUsed()).Msg("Config file loaded")
|
||||
}
|
||||
|
||||
|
||||
// Bind environment variables
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvPrefix("DLC") // DanceLessonsCoach prefix
|
||||
@@ -64,25 +97,65 @@ func LoadConfig() (*Config, error) {
|
||||
v.BindEnv("server.port", "DLC_SERVER_PORT")
|
||||
v.BindEnv("shutdown.timeout", "DLC_SHUTDOWN_TIMEOUT")
|
||||
v.BindEnv("logging.json", "DLC_LOGGING_JSON")
|
||||
|
||||
|
||||
// Telemetry environment variables
|
||||
v.BindEnv("telemetry.enabled", "DLC_TELEMETRY_ENABLED")
|
||||
v.BindEnv("telemetry.otlp_endpoint", "DLC_TELEMETRY_OTLP_ENDPOINT")
|
||||
v.BindEnv("telemetry.service_name", "DLC_TELEMETRY_SERVICE_NAME")
|
||||
v.BindEnv("telemetry.insecure", "DLC_TELEMETRY_INSECURE")
|
||||
v.BindEnv("telemetry.sampler.type", "DLC_TELEMETRY_SAMPLER_TYPE")
|
||||
v.BindEnv("telemetry.sampler.ratio", "DLC_TELEMETRY_SAMPLER_RATIO")
|
||||
|
||||
// Unmarshal into Config struct
|
||||
var config Config
|
||||
if err := v.Unmarshal(&config); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to unmarshal config")
|
||||
return nil, fmt.Errorf("config unmarshal error: %w", err)
|
||||
}
|
||||
|
||||
|
||||
log.Info().
|
||||
Str("host", config.Server.Host).
|
||||
Int("port", config.Server.Port).
|
||||
Dur("shutdown_timeout", config.Shutdown.Timeout).
|
||||
Bool("logging_json", config.Logging.JSON).
|
||||
Bool("telemetry_enabled", config.Telemetry.Enabled).
|
||||
Str("telemetry_service", config.Telemetry.ServiceName).
|
||||
Msg("Configuration loaded")
|
||||
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// GetServerAddress returns the formatted server address (host:port)
|
||||
func (c *Config) GetServerAddress() string {
|
||||
return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// GetTelemetryEnabled returns whether telemetry is enabled
|
||||
func (c *Config) GetTelemetryEnabled() bool {
|
||||
return c.Telemetry.Enabled
|
||||
}
|
||||
|
||||
// GetOTLPEndpoint returns the OTLP endpoint for telemetry
|
||||
func (c *Config) GetOTLPEndpoint() string {
|
||||
return c.Telemetry.OTLPEndpoint
|
||||
}
|
||||
|
||||
// GetServiceName returns the service name for telemetry
|
||||
func (c *Config) GetServiceName() string {
|
||||
return c.Telemetry.ServiceName
|
||||
}
|
||||
|
||||
// GetTelemetryInsecure returns whether to use insecure connection
|
||||
func (c *Config) GetTelemetryInsecure() bool {
|
||||
return c.Telemetry.Insecure
|
||||
}
|
||||
|
||||
// GetSamplerType returns the sampler type
|
||||
func (c *Config) GetSamplerType() string {
|
||||
return c.Telemetry.Sampler.Type
|
||||
}
|
||||
|
||||
// GetSamplerRatio returns the sampler ratio
|
||||
func (c *Config) GetSamplerRatio() float64 {
|
||||
return c.Telemetry.Sampler.Ratio
|
||||
}
|
||||
|
||||
@@ -45,4 +45,4 @@ func (h *apiV1GreetHandler) handleGreetPath(w http.ResponseWriter, r *http.Reque
|
||||
func (h *apiV1GreetHandler) writeJSONResponse(w http.ResponseWriter, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": message})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func NewService() *Service {
|
||||
// Implements the Greeter interface.
|
||||
func (s *Service) Greet(ctx context.Context, name string) string {
|
||||
log.Trace().Ctx(ctx).Str("name", name).Msg("Greet function called")
|
||||
|
||||
|
||||
if name == "" {
|
||||
return "Hello world!"
|
||||
}
|
||||
|
||||
@@ -25,4 +25,4 @@ func TestService_Greet(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"DanceLessonsCoach/pkg/config"
|
||||
"DanceLessonsCoach/pkg/greet"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
@@ -13,14 +14,16 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
router *chi.Mux
|
||||
readyCtx context.Context
|
||||
router *chi.Mux
|
||||
readyCtx context.Context
|
||||
withOTEL bool
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.Config, readyCtx context.Context) *Server {
|
||||
s := &Server{
|
||||
router: chi.NewRouter(),
|
||||
readyCtx: readyCtx,
|
||||
router: chi.NewRouter(),
|
||||
readyCtx: readyCtx,
|
||||
withOTEL: cfg.GetTelemetryEnabled(),
|
||||
}
|
||||
s.setupRoutes()
|
||||
return s
|
||||
@@ -41,7 +44,7 @@ func (s *Server) setupRoutes() {
|
||||
|
||||
// API routes
|
||||
s.router.Route("/api/v1", func(r chi.Router) {
|
||||
r.Use(s.apiMiddlewares()...)
|
||||
r.Use(s.getAllMiddlewares()...)
|
||||
s.registerApiV1Routes(r)
|
||||
})
|
||||
}
|
||||
@@ -54,11 +57,20 @@ func (s *Server) registerApiV1Routes(r chi.Router) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) apiMiddlewares() []func(http.Handler) http.Handler {
|
||||
return []func(http.Handler) http.Handler{
|
||||
// getAllMiddlewares returns all middleware including OpenTelemetry if enabled
|
||||
func (s *Server) getAllMiddlewares() []func(http.Handler) http.Handler {
|
||||
middlewares := []func(http.Handler) http.Handler{
|
||||
middleware.StripSlashes,
|
||||
middleware.Recoverer,
|
||||
}
|
||||
|
||||
if s.withOTEL {
|
||||
middlewares = append(middlewares, func(next http.Handler) http.Handler {
|
||||
return otelhttp.NewHandler(next, "")
|
||||
})
|
||||
}
|
||||
|
||||
return middlewares
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -68,7 +80,7 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info().Msg("Readiness check requested")
|
||||
|
||||
|
||||
select {
|
||||
case <-s.readyCtx.Done():
|
||||
log.Info().Msg("Readiness check: not ready (shutting down)")
|
||||
|
||||
100
pkg/telemetry/telemetry.go
Normal file
100
pkg/telemetry/telemetry.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Package telemetry provides OpenTelemetry instrumentation for the DanceLessonsCoach application
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Setup initializes OpenTelemetry tracing with the given configuration
|
||||
type Setup struct {
|
||||
ServiceName string
|
||||
OTLPEndpoint string
|
||||
Insecure bool
|
||||
SamplerType string
|
||||
SamplerRatio float64
|
||||
}
|
||||
|
||||
// InitializeTracing sets up OpenTelemetry tracing provider
|
||||
func (s *Setup) InitializeTracing(ctx context.Context) (*sdktrace.TracerProvider, error) {
|
||||
// Create OTLP gRPC exporter
|
||||
exporter, err := otlptracegrpc.New(ctx,
|
||||
otlptracegrpc.WithEndpoint(s.OTLPEndpoint),
|
||||
otlptracegrpc.WithInsecure(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create resource with service name
|
||||
res, err := resource.New(ctx,
|
||||
resource.WithAttributes(
|
||||
semconv.ServiceName(s.ServiceName),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create sampler based on configuration
|
||||
sampler := s.getSampler()
|
||||
|
||||
// Create trace provider
|
||||
tp := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithBatcher(exporter),
|
||||
sdktrace.WithResource(res),
|
||||
sdktrace.WithSampler(sampler),
|
||||
)
|
||||
|
||||
// Set global tracer provider and propagator
|
||||
otel.SetTracerProvider(tp)
|
||||
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{},
|
||||
propagation.Baggage{},
|
||||
))
|
||||
|
||||
return tp, nil
|
||||
}
|
||||
|
||||
// Shutdown performs cleanup of the tracer provider
|
||||
func Shutdown(ctx context.Context, tp *sdktrace.TracerProvider) error {
|
||||
if tp == nil {
|
||||
return nil
|
||||
}
|
||||
return tp.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// getSampler returns the appropriate sampler based on configuration
|
||||
func (s *Setup) getSampler() sdktrace.Sampler {
|
||||
switch s.SamplerType {
|
||||
case "always_on":
|
||||
return sdktrace.AlwaysSample()
|
||||
case "always_off":
|
||||
return sdktrace.NeverSample()
|
||||
case "traceidratio":
|
||||
return sdktrace.TraceIDRatioBased(s.SamplerRatio)
|
||||
case "parentbased_always_on":
|
||||
return sdktrace.ParentBased(sdktrace.AlwaysSample())
|
||||
case "parentbased_always_off":
|
||||
return sdktrace.ParentBased(sdktrace.NeverSample())
|
||||
case "parentbased_traceidratio":
|
||||
return sdktrace.ParentBased(sdktrace.TraceIDRatioBased(s.SamplerRatio))
|
||||
default:
|
||||
log.Printf("Unknown sampler type: %s, defaulting to always_on", s.SamplerType)
|
||||
return sdktrace.AlwaysSample()
|
||||
}
|
||||
}
|
||||
|
||||
// GetTracer returns a named tracer from the global provider
|
||||
// Returns a no-op tracer if OpenTelemetry is not initialized
|
||||
func GetTracer(name string) trace.Tracer {
|
||||
return otel.Tracer(name)
|
||||
}
|
||||
Reference in New Issue
Block a user