feat(scaffold): add main.go [skip ci]
This commit is contained in:
215
main.go
Normal file
215
main.go
Normal 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-go3 listening on %s\n", addr)
|
||||||
|
log.Fatal(http.ListenAndServe(addr, otelhttp.NewHandler(mux, "")))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user