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
|
||||
*.test
|
||||
*.out
|
||||
bin/
|
||||
|
||||
# Dependency directories
|
||||
vendor/
|
||||
|
||||
29
AGENTS.md
29
AGENTS.md
@@ -355,12 +355,37 @@ go test ./...
|
||||
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/`
|
||||
- Follow existing patterns and interfaces
|
||||
- Add tests for new functionality
|
||||
|
||||
### 6. Stop and Restart
|
||||
### 7. Stop and Restart
|
||||
```bash
|
||||
./scripts/start-server.sh restart
|
||||
```
|
||||
|
||||
@@ -2,125 +2,26 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"DanceLessonsCoach/pkg/config"
|
||||
"DanceLessonsCoach/pkg/server"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize Zerolog with default console format first
|
||||
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
|
||||
// Load configuration (this will also setup logging)
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
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
|
||||
readyCtx, readyCancel := context.WithCancel(context.Background())
|
||||
defer readyCancel()
|
||||
|
||||
// Start server in goroutine
|
||||
// Create and run server
|
||||
server := server.NewServer(cfg, readyCtx)
|
||||
serverCtx, serverStop := context.WithCancel(ctx)
|
||||
|
||||
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()
|
||||
if err := server.Run(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Server failed")
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,45 @@ logging:
|
||||
# When true, logs are output in JSON format instead of console format
|
||||
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
|
||||
# You can also configure via environment variables with DLC_ prefix:
|
||||
# DLC_SERVER_HOST=0.0.0.0
|
||||
# DLC_SERVER_PORT=8080
|
||||
# DLC_SHUTDOWN_TIMEOUT=30s
|
||||
# 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
|
||||
|
||||
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/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/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-isatty v0.0.20 // 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/viper v1.21.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
|
||||
golang.org/x/net v0.52.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/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/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/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/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
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/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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
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/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=
|
||||
|
||||
@@ -3,24 +3,52 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
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
|
||||
@@ -29,11 +57,24 @@ type Config struct {
|
||||
func LoadConfig() (*Config, error) {
|
||||
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
|
||||
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)
|
||||
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
|
||||
if configFile := os.Getenv("DLC_CONFIG_FILE"); configFile != "" {
|
||||
@@ -64,6 +105,15 @@ 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")
|
||||
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
|
||||
var config Config
|
||||
@@ -72,11 +122,25 @@ func LoadConfig() (*Config, error) {
|
||||
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().
|
||||
Str("host", config.Server.Host).
|
||||
Int("port", config.Server.Port).
|
||||
Dur("shutdown_timeout", config.Shutdown.Timeout).
|
||||
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")
|
||||
|
||||
return &config, nil
|
||||
@@ -86,3 +150,70 @@ func LoadConfig() (*Config, error) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,17 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"DanceLessonsCoach/pkg/config"
|
||||
"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/middleware"
|
||||
@@ -15,12 +22,17 @@ import (
|
||||
type Server struct {
|
||||
router *chi.Mux
|
||||
readyCtx context.Context
|
||||
withOTEL bool
|
||||
config *config.Config
|
||||
tracerProvider *sdktrace.TracerProvider
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.Config, readyCtx context.Context) *Server {
|
||||
s := &Server{
|
||||
router: chi.NewRouter(),
|
||||
readyCtx: readyCtx,
|
||||
router: chi.NewRouter(),
|
||||
readyCtx: readyCtx,
|
||||
withOTEL: cfg.GetTelemetryEnabled(),
|
||||
config: cfg,
|
||||
}
|
||||
s.setupRoutes()
|
||||
return s
|
||||
@@ -41,7 +53,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 +66,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) {
|
||||
@@ -83,3 +104,99 @@ func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) Router() http.Handler {
|
||||
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