Release prep: 54 engines, self-hosted signatures, i18n, dashboard updates

This commit is contained in:
DmitrL-dev 2026-03-23 16:45:40 +10:00
parent 694e32be26
commit 41cbfd6e0a
178 changed files with 36008 additions and 399 deletions

View file

@ -0,0 +1,83 @@
package tracing
import (
"fmt"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
// HTTPMiddleware creates spans for each HTTP request.
// Extracts trace context from incoming headers and sets span attributes.
func HTTPMiddleware(next http.Handler) http.Handler {
tracer := otel.Tracer("sentinel-soc/http")
propagator := otel.GetTextMapPropagator()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract trace context from incoming headers.
ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
spanName := fmt.Sprintf("%s %s", r.Method, r.URL.Path)
ctx, span := tracer.Start(ctx, spanName,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithAttributes(
attribute.String("http.method", r.Method),
attribute.String("http.url", r.URL.String()),
attribute.String("http.target", r.URL.Path),
attribute.String("http.user_agent", r.UserAgent()),
attribute.String("net.host.name", r.Host),
),
)
defer span.End()
// Wrap response writer to capture status code.
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(sw, r.WithContext(ctx))
span.SetAttributes(
attribute.Int("http.status_code", sw.status),
)
if sw.status >= 400 {
span.SetAttributes(attribute.Bool("error", true))
}
})
}
// statusWriter captures the HTTP status code for span attributes.
// Implements http.Flusher to support SSE/streaming through middleware chain.
type statusWriter struct {
http.ResponseWriter
status int
wroteHeader bool
}
func (sw *statusWriter) WriteHeader(code int) {
if !sw.wroteHeader {
sw.status = code
sw.wroteHeader = true
}
sw.ResponseWriter.WriteHeader(code)
}
func (sw *statusWriter) Write(b []byte) (int, error) {
if !sw.wroteHeader {
sw.wroteHeader = true
}
return sw.ResponseWriter.Write(b)
}
// Flush delegates to the underlying ResponseWriter if it supports http.Flusher.
// Required for SSE streaming endpoints to work through the middleware chain.
func (sw *statusWriter) Flush() {
if f, ok := sw.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// Unwrap returns the underlying ResponseWriter for Go 1.20+ ResponseController.
func (sw *statusWriter) Unwrap() http.ResponseWriter {
return sw.ResponseWriter
}

View file

@ -0,0 +1,91 @@
// Package tracing provides OpenTelemetry instrumentation for the SOC platform.
//
// Usage:
//
// OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317 go run ./cmd/soc/
//
// If OTEL_EXPORTER_OTLP_ENDPOINT is not set, tracing is disabled (noop).
package tracing
import (
"context"
"log/slog"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.opentelemetry.io/otel/trace"
)
const (
ServiceName = "sentinel-soc"
ServiceVersion = "1.0.0"
)
// InitTracer sets up the OpenTelemetry TracerProvider with OTLP gRPC exporter.
// Returns the provider (for shutdown) and any error.
// If endpoint is empty, returns a noop provider (safe to use, no overhead).
func InitTracer(ctx context.Context, endpoint string) (*sdktrace.TracerProvider, error) {
if endpoint == "" {
slog.Info("tracing disabled: OTEL_EXPORTER_OTLP_ENDPOINT not set")
return nil, nil
}
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(endpoint),
otlptracegrpc.WithInsecure(), // Use TLS in production
otlptracegrpc.WithTimeout(5*time.Second),
)
if err != nil {
return nil, err
}
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(ServiceName),
semconv.ServiceVersion(ServiceVersion),
),
)
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter,
sdktrace.WithMaxQueueSize(2048),
sdktrace.WithBatchTimeout(5*time.Second),
),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(1.0))),
)
otel.SetTracerProvider(tp)
slog.Info("tracing enabled",
"endpoint", endpoint,
"service", ServiceName,
"version", ServiceVersion,
)
return tp, nil
}
// Tracer returns a named tracer from the global provider.
func Tracer(name string) trace.Tracer {
return otel.Tracer(name)
}
// Shutdown gracefully flushes and stops the tracer provider.
func Shutdown(ctx context.Context, tp *sdktrace.TracerProvider) {
if tp == nil {
return
}
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := tp.Shutdown(shutdownCtx); err != nil {
slog.Error("tracer shutdown error", "error", err)
}
}

View file

@ -0,0 +1,106 @@
package tracing
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- InitTracer Tests ---
func TestInitTracer_NoopWhenEndpointEmpty(t *testing.T) {
tp, err := InitTracer(context.Background(), "")
require.NoError(t, err)
assert.Nil(t, tp, "empty endpoint should return nil TracerProvider (noop)")
}
func TestShutdown_NilProvider_NoPanic(t *testing.T) {
// Should not panic when called with nil.
assert.NotPanics(t, func() {
Shutdown(context.Background(), nil)
})
}
func TestTracer_ReturnsNonNil(t *testing.T) {
tr := Tracer("test-tracer")
assert.NotNil(t, tr)
}
// --- HTTPMiddleware Tests ---
func TestHTTPMiddleware_SetsStatusCode(t *testing.T) {
handler := HTTPMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
w.Write([]byte("created"))
}))
req := httptest.NewRequest(http.MethodPost, "/api/soc/event", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
assert.Equal(t, "created", rr.Body.String())
}
func TestHTTPMiddleware_Default200(t *testing.T) {
handler := HTTPMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}))
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestHTTPMiddleware_ErrorStatus(t *testing.T) {
handler := HTTPMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
req := httptest.NewRequest(http.MethodGet, "/error", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
}
// --- statusWriter Tests ---
func TestStatusWriter_DefaultStatus(t *testing.T) {
rr := httptest.NewRecorder()
sw := &statusWriter{ResponseWriter: rr, status: http.StatusOK}
assert.Equal(t, http.StatusOK, sw.status)
assert.False(t, sw.wroteHeader)
}
func TestStatusWriter_WriteHeaderOnce(t *testing.T) {
rr := httptest.NewRecorder()
sw := &statusWriter{ResponseWriter: rr, status: http.StatusOK}
sw.WriteHeader(http.StatusNotFound)
assert.Equal(t, http.StatusNotFound, sw.status)
assert.True(t, sw.wroteHeader)
// Second call should NOT change status.
sw.WriteHeader(http.StatusCreated)
assert.Equal(t, http.StatusNotFound, sw.status, "status should not change on second WriteHeader")
}
func TestStatusWriter_WriteImplicitHeader(t *testing.T) {
rr := httptest.NewRecorder()
sw := &statusWriter{ResponseWriter: rr, status: http.StatusOK}
n, err := sw.Write([]byte("hello"))
assert.NoError(t, err)
assert.Equal(t, 5, n)
assert.True(t, sw.wroteHeader, "Write should set wroteHeader")
}