Merge pull request 'feature/opentelemetry' (#1) from feature/opentelemetry into main
Reviewed-on: arcodange/DanceLessonsCoach#1
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
|
bin/
|
||||||
|
|
||||||
# Dependency directories
|
# Dependency directories
|
||||||
vendor/
|
vendor/
|
||||||
|
|||||||
29
AGENTS.md
29
AGENTS.md
@@ -355,12 +355,37 @@ go test ./...
|
|||||||
go test ./pkg/greet/
|
go test ./pkg/greet/
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Make Changes
|
### 5. Build Binaries
|
||||||
|
|
||||||
|
The project uses a build script to compile binaries into the `bin/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build both server and greet binaries
|
||||||
|
./scripts/build.sh
|
||||||
|
|
||||||
|
# This creates:
|
||||||
|
# - ./bin/server - The web server binary
|
||||||
|
# - ./bin/greet - The CLI greeting tool
|
||||||
|
```
|
||||||
|
|
||||||
|
**Binary Usage:**
|
||||||
|
```bash
|
||||||
|
# Run the server
|
||||||
|
./bin/server
|
||||||
|
|
||||||
|
# Use the greet CLI
|
||||||
|
./bin/greet # Output: Hello world!
|
||||||
|
./bin/greet John # Output: Hello John!
|
||||||
|
```
|
||||||
|
|
||||||
|
**The `bin/` directory is gitignored** to prevent binary files from being committed to the repository.
|
||||||
|
|
||||||
|
### 6. Make Changes
|
||||||
- Edit source files in `pkg/` or `cmd/`
|
- Edit source files in `pkg/` or `cmd/`
|
||||||
- Follow existing patterns and interfaces
|
- Follow existing patterns and interfaces
|
||||||
- Add tests for new functionality
|
- Add tests for new functionality
|
||||||
|
|
||||||
### 6. Stop and Restart
|
### 7. Stop and Restart
|
||||||
```bash
|
```bash
|
||||||
./scripts/start-server.sh restart
|
./scripts/start-server.sh restart
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ func main() {
|
|||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
name = os.Args[1]
|
name = os.Args[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(service.Greet(context.Background(), name))
|
fmt.Println(service.Greet(context.Background(), name))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,125 +2,26 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"DanceLessonsCoach/pkg/config"
|
"DanceLessonsCoach/pkg/config"
|
||||||
"DanceLessonsCoach/pkg/server"
|
"DanceLessonsCoach/pkg/server"
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Initialize Zerolog with default console format first
|
// Load configuration (this will also setup logging)
|
||||||
zerolog.SetGlobalLevel(zerolog.TraceLevel)
|
|
||||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
|
||||||
consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr}
|
|
||||||
|
|
||||||
// Check if JSON logging is requested via environment variable
|
|
||||||
// This allows JSON logging even during config loading
|
|
||||||
jsonLogging := os.Getenv("DLC_LOGGING_JSON") == "true"
|
|
||||||
|
|
||||||
if jsonLogging {
|
|
||||||
// JSON output for structured logging
|
|
||||||
log.Logger = log.Output(os.Stderr)
|
|
||||||
} else {
|
|
||||||
// Console output for initial logging
|
|
||||||
log.Logger = log.Output(consoleWriter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load configuration
|
|
||||||
cfg, err := config.LoadConfig()
|
cfg, err := config.LoadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Failed to load configuration")
|
log.Fatal().Err(err).Msg("Failed to load configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconfigure logging based on loaded configuration (overrides env var)
|
|
||||||
if cfg.Logging.JSON {
|
|
||||||
// JSON output for structured logging
|
|
||||||
log.Logger = log.Output(os.Stderr)
|
|
||||||
} else {
|
|
||||||
// Keep console output
|
|
||||||
log.Logger = log.Output(consoleWriter)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Bool("json_logging", cfg.Logging.JSON).Msg("Logging configured")
|
|
||||||
|
|
||||||
// Setup signal context for graceful shutdown
|
|
||||||
rootCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
defer stop()
|
|
||||||
|
|
||||||
// Create root context with cancellation
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Create ongoing context for active requests
|
|
||||||
ongoingCtx, stopOngoingGracefully := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
// Create readiness context to control readiness state
|
// Create readiness context to control readiness state
|
||||||
readyCtx, readyCancel := context.WithCancel(context.Background())
|
readyCtx, readyCancel := context.WithCancel(context.Background())
|
||||||
defer readyCancel()
|
defer readyCancel()
|
||||||
|
|
||||||
// Start server in goroutine
|
// Create and run server
|
||||||
server := server.NewServer(cfg, readyCtx)
|
server := server.NewServer(cfg, readyCtx)
|
||||||
serverCtx, serverStop := context.WithCancel(ctx)
|
if err := server.Run(); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Server failed")
|
||||||
go func() {
|
}
|
||||||
log.Info().Str("address", cfg.GetServerAddress()).Msg("Server running")
|
}
|
||||||
|
|
||||||
srv := &http.Server{
|
|
||||||
Addr: cfg.GetServerAddress(),
|
|
||||||
Handler: server.Router(),
|
|
||||||
BaseContext: func(_ net.Listener) context.Context {
|
|
||||||
return ongoingCtx
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the HTTP server in a separate goroutine
|
|
||||||
go func() {
|
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
log.Error().Err(err).Msg("Server error")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for signal
|
|
||||||
<-rootCtx.Done()
|
|
||||||
stop()
|
|
||||||
log.Info().Msg("Shutdown signal received")
|
|
||||||
|
|
||||||
// Cancel readiness context to stop accepting new requests
|
|
||||||
readyCancel()
|
|
||||||
log.Info().Msg("Readiness set to false, no longer accepting new requests")
|
|
||||||
|
|
||||||
// Give time for readiness check to propagate (simplified for our case)
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
log.Info().Msg("Readiness check propagated, now waiting for ongoing requests to finish.")
|
|
||||||
|
|
||||||
// Create shutdown context with timeout from config
|
|
||||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), cfg.Shutdown.Timeout)
|
|
||||||
defer shutdownCancel()
|
|
||||||
|
|
||||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
|
||||||
log.Error().Err(err).Msg("Server shutdown failed")
|
|
||||||
} else {
|
|
||||||
log.Info().Msg("Server shutdown complete")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop ongoing requests context
|
|
||||||
stopOngoingGracefully()
|
|
||||||
cancel()
|
|
||||||
serverStop()
|
|
||||||
log.Info().Msg("Server exited")
|
|
||||||
|
|
||||||
// Force log flush by writing to stderr directly
|
|
||||||
// This ensures logs are written before process exits
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for shutdown
|
|
||||||
<-serverCtx.Done()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,10 +21,46 @@ logging:
|
|||||||
# Enable JSON output for structured logging (default: false)
|
# Enable JSON output for structured logging (default: false)
|
||||||
# When true, logs are output in JSON format instead of console format
|
# When true, logs are output in JSON format instead of console format
|
||||||
json: false
|
json: false
|
||||||
|
|
||||||
|
# Log level (default: "trace")
|
||||||
|
# Options: "trace", "debug", "info", "warn", "error", "fatal", "panic"
|
||||||
|
level: trace
|
||||||
|
|
||||||
|
# Telemetry configuration (OpenTelemetry)
|
||||||
|
telemetry:
|
||||||
|
# Enable OpenTelemetry tracing (default: false)
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# OTLP endpoint for trace export (default: "localhost:4317")
|
||||||
|
# Format: host:port
|
||||||
|
otlp_endpoint: "localhost:4317"
|
||||||
|
|
||||||
|
# Service name for tracing (default: "DanceLessonsCoach")
|
||||||
|
service_name: "DanceLessonsCoach"
|
||||||
|
|
||||||
|
# Use insecure connection (no TLS) (default: true)
|
||||||
|
insecure: true
|
||||||
|
|
||||||
|
# Sampler configuration
|
||||||
|
sampler:
|
||||||
|
# Sampler type (default: "parentbased_always_on")
|
||||||
|
# Options: "always_on", "always_off", "traceidratio", "parentbased_always_on", "parentbased_always_off", "parentbased_traceidratio"
|
||||||
|
type: "parentbased_always_on"
|
||||||
|
|
||||||
|
# Sampling ratio (0.0 to 1.0, default: 1.0)
|
||||||
|
# Only used with traceidratio and parentbased_traceidratio samplers
|
||||||
|
ratio: 1.0
|
||||||
|
|
||||||
# Environment Variables
|
# Environment Variables
|
||||||
# You can also configure via environment variables with DLC_ prefix:
|
# You can also configure via environment variables with DLC_ prefix:
|
||||||
# DLC_SERVER_HOST=0.0.0.0
|
# DLC_SERVER_HOST=0.0.0.0
|
||||||
# DLC_SERVER_PORT=8080
|
# DLC_SERVER_PORT=8080
|
||||||
# DLC_SHUTDOWN_TIMEOUT=30s
|
# DLC_SHUTDOWN_TIMEOUT=30s
|
||||||
# DLC_LOGGING_JSON=false
|
# DLC_LOGGING_JSON=false
|
||||||
|
# DLC_LOGGING_LEVEL=trace
|
||||||
|
# DLC_TELEMETRY_ENABLED=true
|
||||||
|
# DLC_TELEMETRY_OTLP_ENDPOINT="jaeger:4317"
|
||||||
|
# DLC_TELEMETRY_SERVICE_NAME="DanceLessonsCoach"
|
||||||
|
# DLC_TELEMETRY_INSECURE=true
|
||||||
|
# DLC_TELEMETRY_SAMPLER_TYPE="parentbased_always_on"
|
||||||
|
# DLC_TELEMETRY_SAMPLER_RATIO=1.0
|
||||||
|
|||||||
23
go.mod
23
go.mod
@@ -3,9 +3,16 @@ module DanceLessonsCoach
|
|||||||
go 1.26.1
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
@@ -17,7 +24,21 @@ require (
|
|||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/spf13/viper v1.21.0 // indirect
|
github.com/spf13/viper v1.21.0 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
|
google.golang.org/grpc v1.80.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
45
go.sum
45
go.sum
@@ -1,9 +1,24 @@
|
|||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
@@ -26,11 +41,41 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
|||||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
||||||
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||||
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -3,24 +3,52 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config represents the application configuration
|
// Config represents the application configuration
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server struct {
|
Server ServerConfig `mapstructure:"server"`
|
||||||
Host string `mapstructure:"host"`
|
Shutdown ShutdownConfig `mapstructure:"shutdown"`
|
||||||
Port int `mapstructure:"port"`
|
Logging LoggingConfig `mapstructure:"logging"`
|
||||||
}
|
Telemetry TelemetryConfig `mapstructure:"telemetry"`
|
||||||
Shutdown struct {
|
}
|
||||||
Timeout time.Duration `mapstructure:"timeout"`
|
|
||||||
}
|
// ServerConfig holds server-related configuration
|
||||||
Logging struct {
|
type ServerConfig struct {
|
||||||
JSON bool `mapstructure:"json"`
|
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"`
|
||||||
|
Level string `mapstructure:"level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// LoadConfig loads configuration from file, environment variables, and defaults
|
||||||
@@ -29,12 +57,25 @@ type Config struct {
|
|||||||
func LoadConfig() (*Config, error) {
|
func LoadConfig() (*Config, error) {
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
|
|
||||||
|
// Set up initial console logging for config loading messages
|
||||||
|
consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr}
|
||||||
|
log.Logger = log.Output(consoleWriter)
|
||||||
|
|
||||||
// Set default values
|
// Set default values
|
||||||
v.SetDefault("server.host", "0.0.0.0")
|
v.SetDefault("server.host", "0.0.0.0")
|
||||||
v.SetDefault("server.port", 8080)
|
v.SetDefault("server.port", 8080)
|
||||||
v.SetDefault("shutdown.timeout", 30*time.Second)
|
v.SetDefault("shutdown.timeout", 30*time.Second)
|
||||||
v.SetDefault("logging.json", false)
|
v.SetDefault("logging.json", false)
|
||||||
|
v.SetDefault("logging.level", "trace")
|
||||||
|
|
||||||
|
// 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
|
// Check for custom config file path via environment variable
|
||||||
if configFile := os.Getenv("DLC_CONFIG_FILE"); configFile != "" {
|
if configFile := os.Getenv("DLC_CONFIG_FILE"); configFile != "" {
|
||||||
v.SetConfigFile(configFile)
|
v.SetConfigFile(configFile)
|
||||||
@@ -45,7 +86,7 @@ func LoadConfig() (*Config, error) {
|
|||||||
v.SetConfigType("yaml")
|
v.SetConfigType("yaml")
|
||||||
v.AddConfigPath(".")
|
v.AddConfigPath(".")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read config file if it exists
|
// Read config file if it exists
|
||||||
if err := v.ReadInConfig(); err != nil {
|
if err := v.ReadInConfig(); err != nil {
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||||
@@ -56,7 +97,7 @@ func LoadConfig() (*Config, error) {
|
|||||||
} else {
|
} else {
|
||||||
log.Info().Str("config_file", v.ConfigFileUsed()).Msg("Config file loaded")
|
log.Info().Str("config_file", v.ConfigFileUsed()).Msg("Config file loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind environment variables
|
// Bind environment variables
|
||||||
v.AutomaticEnv()
|
v.AutomaticEnv()
|
||||||
v.SetEnvPrefix("DLC") // DanceLessonsCoach prefix
|
v.SetEnvPrefix("DLC") // DanceLessonsCoach prefix
|
||||||
@@ -64,25 +105,115 @@ func LoadConfig() (*Config, error) {
|
|||||||
v.BindEnv("server.port", "DLC_SERVER_PORT")
|
v.BindEnv("server.port", "DLC_SERVER_PORT")
|
||||||
v.BindEnv("shutdown.timeout", "DLC_SHUTDOWN_TIMEOUT")
|
v.BindEnv("shutdown.timeout", "DLC_SHUTDOWN_TIMEOUT")
|
||||||
v.BindEnv("logging.json", "DLC_LOGGING_JSON")
|
v.BindEnv("logging.json", "DLC_LOGGING_JSON")
|
||||||
|
v.BindEnv("logging.level", "DLC_LOGGING_LEVEL")
|
||||||
|
|
||||||
|
// 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
|
// Unmarshal into Config struct
|
||||||
var config Config
|
var config Config
|
||||||
if err := v.Unmarshal(&config); err != nil {
|
if err := v.Unmarshal(&config); err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to unmarshal config")
|
log.Error().Err(err).Msg("Failed to unmarshal config")
|
||||||
return nil, fmt.Errorf("config unmarshal error: %w", err)
|
return nil, fmt.Errorf("config unmarshal error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure log output format (JSON or console) first
|
||||||
|
if config.Logging.JSON {
|
||||||
|
log.Logger = log.Output(os.Stderr)
|
||||||
|
} else {
|
||||||
|
consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr}
|
||||||
|
log.Logger = log.Output(consoleWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup logging based on configuration
|
||||||
|
config.SetupLogging()
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("host", config.Server.Host).
|
Str("host", config.Server.Host).
|
||||||
Int("port", config.Server.Port).
|
Int("port", config.Server.Port).
|
||||||
Dur("shutdown_timeout", config.Shutdown.Timeout).
|
Dur("shutdown_timeout", config.Shutdown.Timeout).
|
||||||
Bool("logging_json", config.Logging.JSON).
|
Bool("logging_json", config.Logging.JSON).
|
||||||
|
Str("logging_level", config.Logging.Level).
|
||||||
|
Bool("telemetry_enabled", config.Telemetry.Enabled).
|
||||||
|
Str("telemetry_service", config.Telemetry.ServiceName).
|
||||||
Msg("Configuration loaded")
|
Msg("Configuration loaded")
|
||||||
|
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetServerAddress returns the formatted server address (host:port)
|
// GetServerAddress returns the formatted server address (host:port)
|
||||||
func (c *Config) GetServerAddress() string {
|
func (c *Config) GetServerAddress() string {
|
||||||
return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLogLevel returns the logging level
|
||||||
|
func (c *Config) GetLogLevel() string {
|
||||||
|
return c.Logging.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupLogging configures zerolog based on the configuration
|
||||||
|
func (c *Config) SetupLogging() {
|
||||||
|
// Parse log level
|
||||||
|
level := parseLogLevel(c.GetLogLevel())
|
||||||
|
zerolog.SetGlobalLevel(level)
|
||||||
|
|
||||||
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLogLevel converts a string log level to zerolog.Level
|
||||||
|
func parseLogLevel(level string) zerolog.Level {
|
||||||
|
switch strings.ToLower(level) {
|
||||||
|
case "trace":
|
||||||
|
return zerolog.TraceLevel
|
||||||
|
case "debug":
|
||||||
|
return zerolog.DebugLevel
|
||||||
|
case "info":
|
||||||
|
return zerolog.InfoLevel
|
||||||
|
case "warn", "warning":
|
||||||
|
return zerolog.WarnLevel
|
||||||
|
case "error":
|
||||||
|
return zerolog.ErrorLevel
|
||||||
|
case "fatal":
|
||||||
|
return zerolog.FatalLevel
|
||||||
|
case "panic":
|
||||||
|
return zerolog.PanicLevel
|
||||||
|
default:
|
||||||
|
log.Warn().Str("level", level).Msg("Unknown log level, defaulting to trace")
|
||||||
|
return zerolog.TraceLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,4 +45,4 @@ func (h *apiV1GreetHandler) handleGreetPath(w http.ResponseWriter, r *http.Reque
|
|||||||
func (h *apiV1GreetHandler) writeJSONResponse(w http.ResponseWriter, message string) {
|
func (h *apiV1GreetHandler) writeJSONResponse(w http.ResponseWriter, message string) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]string{"message": message})
|
json.NewEncoder(w).Encode(map[string]string{"message": message})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func NewService() *Service {
|
|||||||
// Implements the Greeter interface.
|
// Implements the Greeter interface.
|
||||||
func (s *Service) Greet(ctx context.Context, name string) string {
|
func (s *Service) Greet(ctx context.Context, name string) string {
|
||||||
log.Trace().Ctx(ctx).Str("name", name).Msg("Greet function called")
|
log.Trace().Ctx(ctx).Str("name", name).Msg("Greet function called")
|
||||||
|
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return "Hello world!"
|
return "Hello world!"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,4 +25,4 @@ func TestService_Greet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,17 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"DanceLessonsCoach/pkg/config"
|
"DanceLessonsCoach/pkg/config"
|
||||||
"DanceLessonsCoach/pkg/greet"
|
"DanceLessonsCoach/pkg/greet"
|
||||||
|
"DanceLessonsCoach/pkg/telemetry"
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
@@ -15,12 +22,17 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
router *chi.Mux
|
router *chi.Mux
|
||||||
readyCtx context.Context
|
readyCtx context.Context
|
||||||
|
withOTEL bool
|
||||||
|
config *config.Config
|
||||||
|
tracerProvider *sdktrace.TracerProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(cfg *config.Config, readyCtx context.Context) *Server {
|
func NewServer(cfg *config.Config, readyCtx context.Context) *Server {
|
||||||
s := &Server{
|
s := &Server{
|
||||||
router: chi.NewRouter(),
|
router: chi.NewRouter(),
|
||||||
readyCtx: readyCtx,
|
readyCtx: readyCtx,
|
||||||
|
withOTEL: cfg.GetTelemetryEnabled(),
|
||||||
|
config: cfg,
|
||||||
}
|
}
|
||||||
s.setupRoutes()
|
s.setupRoutes()
|
||||||
return s
|
return s
|
||||||
@@ -41,7 +53,7 @@ func (s *Server) setupRoutes() {
|
|||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
s.router.Route("/api/v1", func(r chi.Router) {
|
s.router.Route("/api/v1", func(r chi.Router) {
|
||||||
r.Use(s.apiMiddlewares()...)
|
r.Use(s.getAllMiddlewares()...)
|
||||||
s.registerApiV1Routes(r)
|
s.registerApiV1Routes(r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -54,11 +66,20 @@ func (s *Server) registerApiV1Routes(r chi.Router) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) apiMiddlewares() []func(http.Handler) http.Handler {
|
// getAllMiddlewares returns all middleware including OpenTelemetry if enabled
|
||||||
return []func(http.Handler) http.Handler{
|
func (s *Server) getAllMiddlewares() []func(http.Handler) http.Handler {
|
||||||
|
middlewares := []func(http.Handler) http.Handler{
|
||||||
middleware.StripSlashes,
|
middleware.StripSlashes,
|
||||||
middleware.Recoverer,
|
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) {
|
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -68,7 +89,7 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Info().Msg("Readiness check requested")
|
log.Info().Msg("Readiness check requested")
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-s.readyCtx.Done():
|
case <-s.readyCtx.Done():
|
||||||
log.Info().Msg("Readiness check: not ready (shutting down)")
|
log.Info().Msg("Readiness check: not ready (shutting down)")
|
||||||
@@ -83,3 +104,99 @@ func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) Router() http.Handler {
|
func (s *Server) Router() http.Handler {
|
||||||
return s.router
|
return s.router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run starts the HTTP server and handles graceful shutdown
|
||||||
|
func (s *Server) Run() error {
|
||||||
|
// Initialize OpenTelemetry if enabled
|
||||||
|
var err error
|
||||||
|
if s.withOTEL {
|
||||||
|
log.Info().Msg("Initializing OpenTelemetry tracing")
|
||||||
|
|
||||||
|
telemetrySetup := &telemetry.Setup{
|
||||||
|
ServiceName: s.config.GetServiceName(),
|
||||||
|
OTLPEndpoint: s.config.GetOTLPEndpoint(),
|
||||||
|
Insecure: s.config.GetTelemetryInsecure(),
|
||||||
|
SamplerType: s.config.GetSamplerType(),
|
||||||
|
SamplerRatio: s.config.GetSamplerRatio(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.tracerProvider, err = telemetrySetup.InitializeTracing(context.Background()); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to initialize OpenTelemetry, continuing without tracing")
|
||||||
|
s.withOTEL = false
|
||||||
|
} else {
|
||||||
|
log.Info().Msg("OpenTelemetry tracing initialized successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup signal context for graceful shutdown
|
||||||
|
rootCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
// Create ongoing context for active requests
|
||||||
|
ongoingCtx, stopOngoingGracefully := context.WithCancel(context.Background())
|
||||||
|
defer stopOngoingGracefully()
|
||||||
|
|
||||||
|
// Create HTTP server
|
||||||
|
log.Info().Str("address", s.config.GetServerAddress()).Msg("Server running")
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: s.config.GetServerAddress(),
|
||||||
|
Handler: s.router,
|
||||||
|
BaseContext: func(_ net.Listener) context.Context {
|
||||||
|
return ongoingCtx
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the HTTP server in a separate goroutine
|
||||||
|
serverErrors := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
serverErrors <- err
|
||||||
|
}
|
||||||
|
close(serverErrors)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for signal
|
||||||
|
<-rootCtx.Done()
|
||||||
|
stop()
|
||||||
|
log.Info().Msg("Shutdown signal received")
|
||||||
|
|
||||||
|
// Cancel readiness context to stop accepting new requests
|
||||||
|
if cancelReady, ok := s.readyCtx.(interface{ Cancel() }); ok {
|
||||||
|
cancelReady.Cancel()
|
||||||
|
}
|
||||||
|
log.Info().Msg("Readiness set to false, no longer accepting new requests")
|
||||||
|
|
||||||
|
// Give time for readiness check to propagate
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
log.Info().Msg("Readiness check propagated, now waiting for ongoing requests to finish.")
|
||||||
|
|
||||||
|
// Create shutdown context with timeout from config
|
||||||
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), s.config.Shutdown.Timeout)
|
||||||
|
defer shutdownCancel()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Server shutdown failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Info().Msg("Server shutdown complete")
|
||||||
|
|
||||||
|
// Shutdown OpenTelemetry tracer provider
|
||||||
|
if s.tracerProvider != nil {
|
||||||
|
if err := telemetry.Shutdown(context.Background(), s.tracerProvider); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to shutdown OpenTelemetry tracer provider")
|
||||||
|
} else {
|
||||||
|
log.Info().Msg("OpenTelemetry tracer provider shutdown complete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force log flush
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Return any server errors
|
||||||
|
if err, ok := <-serverErrors; ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
26
scripts/build.sh
Executable file
26
scripts/build.sh
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# DanceLessonsCoach Build Script
|
||||||
|
# Builds binaries into the bin/ directory
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔨 Building DanceLessonsCoach binaries..."
|
||||||
|
|
||||||
|
# Create bin directory if it doesn't exist
|
||||||
|
mkdir -p bin
|
||||||
|
|
||||||
|
# Build server binary
|
||||||
|
echo "📦 Building server..."
|
||||||
|
go build -o bin/server ./cmd/server
|
||||||
|
|
||||||
|
# Build greet CLI binary
|
||||||
|
echo "📦 Building greet CLI..."
|
||||||
|
go build -o bin/greet ./cmd/greet
|
||||||
|
|
||||||
|
echo "✅ Build complete!"
|
||||||
|
echo " Server binary: ./bin/server"
|
||||||
|
echo " Greet binary: ./bin/greet"
|
||||||
|
echo ""
|
||||||
|
echo "💡 To run the server: ./bin/server"
|
||||||
|
echo "💡 To use the greet CLI: ./bin/greet [name]"
|
||||||
92
scripts/test-opentelemetry.sh
Executable file
92
scripts/test-opentelemetry.sh
Executable file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# DanceLessonsCoach OpenTelemetry Test Script
|
||||||
|
# This script tests OpenTelemetry integration with Jaeger
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo -e "\033[1;34m=== DanceLessonsCoach OpenTelemetry Test ===\033[0m"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
PROJECT_DIR="/Users/gabrielradureau/Work/Vibe/DanceLessonsCoach"
|
||||||
|
SERVER_CMD="./scripts/start-server.sh"
|
||||||
|
LOG_FILE="server.log"
|
||||||
|
PID_FILE="server.pid"
|
||||||
|
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
# Clean up any existing server
|
||||||
|
echo "Cleaning up any existing server..."
|
||||||
|
if [ -f "$PID_FILE" ]; then
|
||||||
|
echo "Found existing PID file, stopping previous server..."
|
||||||
|
$SERVER_CMD stop > /dev/null 2>&1 || true
|
||||||
|
rm -f "$PID_FILE" "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Kill any processes on port 8080
|
||||||
|
pkill -9 -f "go run" > /dev/null 2>&1 || true
|
||||||
|
lsof -ti :8080 | xargs -I {} kill -9 {} > /dev/null 2>&1 || true
|
||||||
|
lsof -ti :4317 | xargs -I {} kill -9 {} > /dev/null 2>&1 || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo "Starting Jaeger in Docker..."
|
||||||
|
# Start Jaeger container if not running
|
||||||
|
if ! docker ps | grep -q "jaegertracing/all-in-one"; then
|
||||||
|
docker run -d --name jaeger \
|
||||||
|
-e COLLECTOR_OTLP_ENABLED=true \
|
||||||
|
-p 16686:16686 \
|
||||||
|
-p 4317:4317 \
|
||||||
|
-p 4318:4318 \
|
||||||
|
jaegertracing/all-in-one:latest
|
||||||
|
echo "Jaeger container started"
|
||||||
|
sleep 5
|
||||||
|
else
|
||||||
|
echo "Jaeger container already running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting server with OpenTelemetry enabled..."
|
||||||
|
DLC_TELEMETRY_ENABLED=true DLC_TELEMETRY_OTLP_ENDPOINT="localhost:4317" DLC_TELEMETRY_INSECURE=true \
|
||||||
|
DLC_TELEMETRY_SERVICE_NAME="DanceLessonsCoach" $SERVER_CMD start
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
echo "Testing API endpoints..."
|
||||||
|
|
||||||
|
# Test health endpoint
|
||||||
|
echo "Testing /api/health:"
|
||||||
|
HEALTH_RESPONSE=$(curl -s http://localhost:8080/api/health)
|
||||||
|
echo "Response: $HEALTH_RESPONSE"
|
||||||
|
|
||||||
|
# Test greet endpoint
|
||||||
|
echo "Testing /api/v1/greet/:"
|
||||||
|
GREET_RESPONSE=$(curl -s http://localhost:8080/api/v1/greet/)
|
||||||
|
echo "Response: $GREET_RESPONSE"
|
||||||
|
|
||||||
|
# Test greet with name
|
||||||
|
echo "Testing /api/v1/greet/TestUser:"
|
||||||
|
GREET_NAME_RESPONSE=$(curl -s http://localhost:8080/api/v1/greet/TestUser)
|
||||||
|
echo "Response: $GREET_NAME_RESPONSE"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Stopping server..."
|
||||||
|
$SERVER_CMD stop
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[0;32m✅ OpenTelemetry Test Complete!\033[0m"
|
||||||
|
echo ""
|
||||||
|
echo "To view traces in Jaeger:"
|
||||||
|
echo "1. Open http://localhost:16686 in your browser"
|
||||||
|
echo "2. Select 'DanceLessonsCoach' service"
|
||||||
|
echo "3. Click 'Find Traces' button"
|
||||||
|
echo ""
|
||||||
|
echo "You should see traces for:"
|
||||||
|
echo "- /api/health requests"
|
||||||
|
echo "- /api/v1/greet/ requests"
|
||||||
|
echo "- /api/v1/greet/TestUser requests"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
rm -f "$PID_FILE" "$LOG_FILE"
|
||||||
|
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user