feat(scaffold): add main.go [skip ci]

This commit is contained in:
2026-05-07 08:31:13 +00:00
parent 077273acdf
commit 554b37e2c7

215
main.go Normal file
View File

@@ -0,0 +1,215 @@
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-go2 listening on %s\n", addr)
log.Fatal(http.ListenAndServe(addr, otelhttp.NewHandler(mux, "")))
}