diff --git a/examples/dice/instrumented/main.go b/examples/dice/instrumented/main.go index a0cfb310612..82c0b97779b 100644 --- a/examples/dice/instrumented/main.go +++ b/examples/dice/instrumented/main.go @@ -40,8 +40,12 @@ func run() error { }() // Start HTTP server. + port := os.Getenv("APPLICATION_PORT") + if port == "" { + port = "8080" + } srv := &http.Server{ - Addr: ":8080", + Addr: ":" + port, BaseContext: func(net.Listener) context.Context { return ctx }, ReadTimeout: time.Second, WriteTimeout: 10 * time.Second, @@ -72,8 +76,7 @@ func newHTTPHandler() http.Handler { mux := http.NewServeMux() // Register handlers. - mux.Handle("/rolldice", http.HandlerFunc(rolldice)) - mux.Handle("/rolldice/{player}", http.HandlerFunc(rolldice)) + mux.Handle("/rolldice", http.HandlerFunc(handleRolldice)) // Add HTTP instrumentation for the whole server. handler := otelhttp.NewHandler(mux, "/") diff --git a/examples/dice/instrumented/otel.go b/examples/dice/instrumented/otel.go index b0e0e58f29d..af55dcaac33 100644 --- a/examples/dice/instrumented/otel.go +++ b/examples/dice/instrumented/otel.go @@ -16,6 +16,7 @@ import ( "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/log" "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/trace" ) @@ -42,12 +43,23 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) { err = errors.Join(inErr, shutdown(ctx)) } + res, resErr := resource.New(ctx, + resource.WithFromEnv(), // reads OTEL_SERVICE_NAME & OTEL_RESOURCE_ATTRIBUTES. + resource.WithProcess(), + resource.WithHost(), + resource.WithTelemetrySDK(), + ) + if resErr != nil { + handleErr(resErr) + return shutdown, resErr + } + // Set up propagator. prop := newPropagator() otel.SetTextMapPropagator(prop) // Set up trace provider. - tracerProvider, err := newtracerProvider() + tracerProvider, err := newTracerProvider(res) if err != nil { handleErr(err) return shutdown, err @@ -56,7 +68,7 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) { otel.SetTracerProvider(tracerProvider) // Set up meter provider. - meterProvider, err := newMeterProvider() + meterProvider, err := newMeterProvider(res) if err != nil { handleErr(err) return shutdown, err @@ -65,7 +77,7 @@ func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) { otel.SetMeterProvider(meterProvider) // Set up logger provider. - loggerProvider, err := newLoggerProvider() + loggerProvider, err := newLoggerProvider(res) if err != nil { handleErr(err) return shutdown, err @@ -83,36 +95,33 @@ func newPropagator() propagation.TextMapPropagator { ) } -func newtracerProvider() (*trace.TracerProvider, error) { - traceExporter, err := stdouttrace.New( - stdouttrace.WithPrettyPrint()) +func newTracerProvider(res *resource.Resource) (*trace.TracerProvider, error) { + traceExporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) if err != nil { return nil, err } tracerProvider := trace.NewTracerProvider( - trace.WithBatcher(traceExporter, - // Default is 5s. Set to 1s for demonstrative purposes. - trace.WithBatchTimeout(time.Second)), + trace.WithBatcher(traceExporter, trace.WithBatchTimeout(time.Second)), // Default is 5s. Set to 1s for demonstrative purposes. + trace.WithResource(res), ) return tracerProvider, nil } -func newMeterProvider() (*metric.MeterProvider, error) { +func newMeterProvider(res *resource.Resource) (*metric.MeterProvider, error) { metricExporter, err := stdoutmetric.New() if err != nil { return nil, err } meterProvider := metric.NewMeterProvider( - metric.WithReader(metric.NewPeriodicReader(metricExporter, - // Default is 1m. Set to 3s for demonstrative purposes. - metric.WithInterval(3*time.Second))), + metric.WithReader(metric.NewPeriodicReader(metricExporter, metric.WithInterval(3*time.Second))), // Default is 1m. Set to 3s for demonstrative purposes. + metric.WithResource(res), ) return meterProvider, nil } -func newLoggerProvider() (*log.LoggerProvider, error) { +func newLoggerProvider(res *resource.Resource) (*log.LoggerProvider, error) { logExporter, err := stdoutlog.New() if err != nil { return nil, err @@ -120,6 +129,7 @@ func newLoggerProvider() (*log.LoggerProvider, error) { loggerProvider := log.NewLoggerProvider( log.WithProcessor(log.NewBatchProcessor(logExporter)), + log.WithResource(res), ) return loggerProvider, nil } diff --git a/examples/dice/instrumented/rolldice.go b/examples/dice/instrumented/rolldice.go index 10edcfd4a91..32ecbb756aa 100644 --- a/examples/dice/instrumented/rolldice.go +++ b/examples/dice/instrumented/rolldice.go @@ -4,10 +4,13 @@ package main import ( - "io" - "math/rand" + "context" + "encoding/json" + "errors" + "math/rand/v2" "net/http" "strconv" + "sync/atomic" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -19,42 +22,163 @@ import ( const name = "go.opentelemetry.io/contrib/examples/dice" var ( - tracer = otel.Tracer(name) - meter = otel.Meter(name) - logger = otelslog.NewLogger(name) - rollCnt metric.Int64Counter + tracer = otel.Tracer(name) + meter = otel.Meter(name) + logger = otelslog.NewLogger(name) + rollCnt metric.Int64Counter + outcomeHist metric.Int64Histogram + lastRollsGauge metric.Int64ObservableGauge + lastRolls atomic.Int64 ) func init() { var err error rollCnt, err = meter.Int64Counter("dice.rolls", - metric.WithDescription("The number of rolls by roll value"), + metric.WithDescription("The number of rolls"), metric.WithUnit("{roll}")) if err != nil { panic(err) } + + outcomeHist, err = meter.Int64Histogram( + "dice.outcome", + metric.WithDescription("Distribution of dice outcomes (1-6)"), + metric.WithUnit("{count}"), + ) + if err != nil { + panic(err) + } + + lastRollsGauge, err = meter.Int64ObservableGauge( + "dice.last.rolls", + metric.WithDescription("The last rolls value observed"), + ) + if err != nil { + panic(err) + } + + // Register the gauge callback. + _, err = meter.RegisterCallback( + func(_ context.Context, o metric.Observer) error { + o.ObserveInt64(lastRollsGauge, lastRolls.Load()) + return nil + }, + lastRollsGauge, + ) + if err != nil { + panic(err) + } } -func rolldice(w http.ResponseWriter, r *http.Request) { - ctx, span := tracer.Start(r.Context(), "roll") - defer span.End() +func handleRolldice(w http.ResponseWriter, r *http.Request) { + // Parse query parameters. + rollsParam := r.URL.Query().Get("rolls") + player := r.URL.Query().Get("player") + + // Default rolls = 1 if not defined. + if rollsParam == "" { + rollsParam = "1" + } - roll := 1 + rand.Intn(6) //nolint:gosec // G404: Use of weak random number generator (math/rand instead of crypto/rand) is ignored as this is not security-sensitive. + // Check if rolls is a number. + rolls, err := strconv.Atoi(rollsParam) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + msg := "Parameter rolls must be a positive integer" + _ = json.NewEncoder(w).Encode(map[string]string{ + "status": "error", + "message": msg, + }) + logger.WarnContext(r.Context(), msg) + return + } - var msg string - if player := r.PathValue("player"); player != "" { - msg = player + " is rolling the dice" + results, err := rollDice(r.Context(), rolls) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + msg := "Internal server error" + _ = json.NewEncoder(w).Encode(map[string]string{ + "status": "error", + "message": msg, + }) + logger.ErrorContext(r.Context(), err.Error()) + return + } + + if player == "" { + logger.DebugContext(r.Context(), "anonymous player rolled", "results", results) } else { - msg = "Anonymous player is rolling the dice" + logger.DebugContext(r.Context(), "player rolled dice", "player", player, "results", results) } - logger.InfoContext(ctx, msg, "result", roll) + logger.InfoContext(r.Context(), "Some player rolled a dice.") + + w.Header().Set("Content-Type", "application/json") + if len(results) == 1 { + writeJSON(r.Context(), w, results[0]) + } else { + writeJSON(r.Context(), w, results) + } +} + +func writeJSON(ctx context.Context, w http.ResponseWriter, v any) { + data, err := json.Marshal(v) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(map[string]string{ + "status": "error", + "message": "Internal Server Error", + }) + logger.ErrorContext(ctx, "json encode failed", "error", err) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(data) +} + +func rollDice(ctx context.Context, rolls int) ([]int, error) { + const maxRolls = 1000 // Arbitrary limit to prevent Slice memory allocation with excessive size value. + + ctx, span := tracer.Start(ctx, "rollDice") + defer span.End() + + if rolls > maxRolls { + err := errors.New("rolls parameter exceeds maximum allowed value") + span.RecordError(err) + return nil, err + } + + if rolls <= 0 { + err := errors.New("rolls must be positive") + span.RecordError(err) + return nil, err + } + + results := make([]int, rolls) + for i := range rolls { + results[i] = rollOnce(ctx) + outcomeHist.Record(ctx, int64(results[i])) + } + + rollsAttr := attribute.Int("rolls", rolls) + span.SetAttributes(rollsAttr) + rollCnt.Add(ctx, int64(rolls), metric.WithAttributes(rollsAttr)) + lastRolls.Store(int64(rolls)) + return results, nil +} + +// rollOnce returns a random number between 1 and 6. +func rollOnce(ctx context.Context) int { + _, span := tracer.Start(ctx, "rollOnce") + defer span.End() + + roll := 1 + rand.IntN(6) //nolint:gosec // G404: Use of weak random number generator (math/rand instead of crypto/rand) is ignored as this is not security-sensitive. rollValueAttr := attribute.Int("roll.value", roll) span.SetAttributes(rollValueAttr) - rollCnt.Add(ctx, 1, metric.WithAttributes(rollValueAttr)) - resp := strconv.Itoa(roll) + "\n" - if _, err := io.WriteString(w, resp); err != nil { - logger.ErrorContext(ctx, "Write failed", "error", err) - } + return roll } diff --git a/examples/dice/uninstrumented/main.go b/examples/dice/uninstrumented/main.go index 87bf30c9cae..0616c0921b3 100644 --- a/examples/dice/uninstrumented/main.go +++ b/examples/dice/uninstrumented/main.go @@ -27,8 +27,13 @@ func run() (err error) { defer stop() // Start HTTP server. + port := os.Getenv("APPLICATION_PORT") + if port == "" { + port = "8080" + } + srv := &http.Server{ - Addr: ":8080", + Addr: ":" + port, BaseContext: func(net.Listener) context.Context { return ctx }, ReadTimeout: time.Second, WriteTimeout: 10 * time.Second, @@ -60,8 +65,6 @@ func newHTTPHandler() http.Handler { mux := http.NewServeMux() // Register handlers. - mux.HandleFunc("/rolldice/", rolldice) - mux.HandleFunc("/rolldice/{player}", rolldice) - + mux.HandleFunc("/rolldice", handleRolldice) return mux } diff --git a/examples/dice/uninstrumented/rolldice.go b/examples/dice/uninstrumented/rolldice.go index 95ac6b4a516..30e5e786158 100644 --- a/examples/dice/uninstrumented/rolldice.go +++ b/examples/dice/uninstrumented/rolldice.go @@ -4,26 +4,100 @@ package main import ( - "io" + "encoding/json" + "errors" "log" "math/rand" "net/http" "strconv" ) -func rolldice(w http.ResponseWriter, r *http.Request) { - roll := 1 + rand.Intn(6) //nolint:gosec // G404: Use of weak random number generator (math/rand instead of crypto/rand) is ignored as this is not security-sensitive. +func handleRolldice(w http.ResponseWriter, r *http.Request) { + // Parse query parameters. + rollsParam := r.URL.Query().Get("rolls") + player := r.URL.Query().Get("player") + + // Default rolls = 1 if not defined. + if rollsParam == "" { + rollsParam = "1" + } + + // Check if rolls is a number. + rolls, err := strconv.Atoi(rollsParam) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + msg := "Parameter rolls must be a positive integer" + _ = json.NewEncoder(w).Encode(map[string]string{ + "status": "error", + "message": msg, + }) + log.Printf("WARN: %s", msg) + return + } + + results, err := rolldice(rolls) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + log.Printf("ERROR: %v", err) + return + } + + if player == "" { + log.Printf("DEBUG: anonymous player rolled %v", results) + } else { + log.Printf("DEBUG: player=%s rolled %v", player, results) + } + log.Printf("INFO: %s %s -> 200 OK", r.Method, r.URL.String()) - var msg string - if player := r.PathValue("player"); player != "" { - msg = player + " is rolling the dice" + if len(results) == 1 { + writeJSON(w, results[0]) } else { - msg = "Anonymous player is rolling the dice" + writeJSON(w, results) + } +} + +func writeJSON(w http.ResponseWriter, v any) { + data, err := json.Marshal(v) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(map[string]string{ + "status": "error", + "message": "Internal Server Error", + }) + log.Printf("ERROR: %v", err) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(data) +} + +func rolldice(rolls int) ([]int, error) { + const maxRolls = 1000 // Arbitrary limit to prevent Slice memory allocation with excessive size value. + + if rolls > maxRolls { + return nil, errors.New("rolls parameter exceeds maximum allowed value") + } + + if rolls <= 0 { + return nil, errors.New("rolls must be positive") + } + + if rolls == 1 { + return []int{rollOnce()}, nil } - log.Printf("%s, result: %d", msg, roll) - resp := strconv.Itoa(roll) + "\n" - if _, err := io.WriteString(w, resp); err != nil { - log.Printf("Write failed: %v", err) + results := make([]int, rolls) + for i := range rolls { + results[i] = rollOnce() } + return results, nil +} + +// rollOnce returns a random number between 1 and 6. +func rollOnce() int { + roll := 1 + rand.Intn(6) //nolint:gosec // G404: Use of weak random number generator (math/rand instead of crypto/rand) is ignored as this is not security-sensitive. + return roll }