216 lines
6.1 KiB
Go
216 lines
6.1 KiB
Go
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-go3 listening on %s\n", addr)
|
|
log.Fatal(http.ListenAndServe(addr, otelhttp.NewHandler(mux, "")))
|
|
}
|