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-go listening on %s\n", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, otelhttp.NewHandler(mux, "")))
|
||||
}
|
||||
Reference in New Issue
Block a user