package main import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "strconv" "strings" "sync" "sync/atomic" "github.com/prometheus/client_golang/prometheus/promhttp" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/propagation" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" ) // --------------------------------------------------------------------------- // Domain model // --------------------------------------------------------------------------- type Item struct { ID int `json:"id"` Name string `json:"name"` Description string `json:"description"` } type ItemRequest struct { Name string `json:"name"` Description string `json:"description"` } // --------------------------------------------------------------------------- // In-memory store // --------------------------------------------------------------------------- var ( store sync.Map counter atomic.Int64 ) func nextID() int { return int(counter.Add(1)) } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func itemIDFromPath(path string) (int, bool) { parts := strings.Split(strings.TrimSuffix(path, "/"), "/") if len(parts) < 3 { return 0, false } id, err := strconv.Atoi(parts[len(parts)-1]) return id, err == nil } // --------------------------------------------------------------------------- // Handlers // --------------------------------------------------------------------------- func healthHandler(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "UP"}) } func itemsHandler(w http.ResponseWriter, r *http.Request) { // Route: /api/items or /api/items/{id} hasSub := strings.Count(strings.TrimSuffix(r.URL.Path, "/"), "/") >= 3 if hasSub { id, ok := itemIDFromPath(r.URL.Path) if !ok { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"}) return } switch r.Method { case http.MethodGet: v, exists := store.Load(id) if !exists { writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) return } writeJSON(w, http.StatusOK, v) case http.MethodPut: var req ItemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } existingRaw, exists := store.Load(id) if !exists { writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) return } existing := existingRaw.(Item) if req.Name == "" { req.Name = existing.Name } if req.Description == "" { req.Description = existing.Description } updated := Item{ID: id, Name: req.Name, Description: req.Description} store.Store(id, updated) writeJSON(w, http.StatusOK, updated) case http.MethodDelete: store.Delete(id) writeJSON(w, http.StatusOK, map[string]int{"deleted": id}) default: w.WriteHeader(http.StatusMethodNotAllowed) } return } // /api/items — list or create switch r.Method { case http.MethodGet: var items []Item store.Range(func(_, v any) bool { items = append(items, v.(Item)) return true }) if items == nil { items = []Item{} } writeJSON(w, http.StatusOK, items) case http.MethodPost: var req ItemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } if req.Name == "" { req.Name = "unnamed" } item := Item{ID: nextID(), Name: req.Name, Description: req.Description} store.Store(item.ID, item) writeJSON(w, http.StatusCreated, item) default: w.WriteHeader(http.StatusMethodNotAllowed) } } // --------------------------------------------------------------------------- // OpenTelemetry setup // --------------------------------------------------------------------------- // setupOTel initialises the OTLP trace and metric pipelines from OTEL_* env // vars (injected by score.yaml / Humanitec). Returns a no-op shutdown func // when OTEL_EXPORTER_OTLP_ENDPOINT is not set (local dev). func setupOTel(ctx context.Context) func() { if os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") == "" { return func() {} } svcName := os.Getenv("OTEL_SERVICE_NAME") if svcName == "" { svcName = "unknown" } res, _ := resource.New(ctx, resource.WithAttributes(semconv.ServiceName(svcName)), resource.WithFromEnv(), ) traceExp, _ := otlptracehttp.New(ctx) tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(traceExp), sdktrace.WithResource(res)) otel.SetTracerProvider(tp) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, )) metricExp, _ := otlpmetrichttp.New(ctx) mp := sdkmetric.NewMeterProvider( sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp)), sdkmetric.WithResource(res), ) otel.SetMeterProvider(mp) return func() { _ = tp.Shutdown(context.Background()) _ = mp.Shutdown(context.Background()) } } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- func main() { ctx := context.Background() shutdownOTel := setupOTel(ctx) defer shutdownOTel() mux := http.NewServeMux() mux.HandleFunc("/health", healthHandler) mux.Handle("/metrics", promhttp.Handler()) mux.HandleFunc("/api/items/", itemsHandler) mux.HandleFunc("/api/items", itemsHandler) addr := ":8080" fmt.Printf("sonar-test-go listening on %s\n", addr) log.Fatal(http.ListenAndServe(addr, otelhttp.NewHandler(mux, ""))) }