初始提交:边缘AI算力机统一AI通讯层
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
CI / security-scan (push) Has been cancelled

This commit is contained in:
freedakgmail
2026-08-03 07:44:05 +08:00
commit 93a469061d
51 changed files with 11565 additions and 0 deletions
+319
View File
@@ -0,0 +1,319 @@
package observability
import (
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"time"
)
// LogLevel represents logging severity levels.
type LogLevel int
const (
LevelDebug LogLevel = iota
LevelInfo
LevelWarn
LevelError
)
func (l LogLevel) String() string {
switch l {
case LevelDebug:
return "DEBUG"
case LevelInfo:
return "INFO"
case LevelWarn:
return "WARN"
case LevelError:
return "ERROR"
default:
return "INFO"
}
}
// ParseLogLevel parses a string to LogLevel.
func ParseLogLevel(s string) LogLevel {
switch strings.ToLower(s) {
case "debug":
return LevelDebug
case "info":
return LevelInfo
case "warn", "warning":
return LevelWarn
case "error":
return LevelError
default:
return LevelInfo
}
}
// LogEntry is a structured JSON log entry.
type LogEntry struct {
Timestamp string `json:"timestamp"`
Level string `json:"level"`
Event string `json:"event,omitempty"`
Message string `json:"message,omitempty"`
RequestID string `json:"request_id,omitempty"`
TaskID string `json:"task_id,omitempty"`
SessionID string `json:"session_id,omitempty"`
TraceID string `json:"trace_id,omitempty"`
Application string `json:"application,omitempty"`
TenantID string `json:"tenant_id,omitempty"`
UserID string `json:"user_id,omitempty"`
FromState string `json:"from_state,omitempty"`
ToState string `json:"to_state,omitempty"`
Reason string `json:"reason,omitempty"`
LogicalModel string `json:"logical_model,omitempty"`
ActualModel string `json:"actual_model,omitempty"`
NodeID string `json:"node_id,omitempty"`
Degraded bool `json:"degraded,omitempty"`
Extra map[string]any `json:"extra,omitempty"`
}
// Logger is a structured JSON logger with sensitive field masking.
type Logger struct {
mu sync.RWMutex
level LogLevel
output *os.File
maskFields []string
promptLogging string
}
var defaultLogger *Logger
func init() {
defaultLogger = NewLogger(LevelInfo, os.Stdout, "metadata_only")
}
// NewLogger creates a new Logger instance.
func NewLogger(level LogLevel, out *os.File, promptLogging string) *Logger {
return &Logger{
level: level,
output: out,
maskFields: []string{"api_key", "apikey", "authorization", "jwt", "secret", "password", "token"},
promptLogging: promptLogging,
}
}
// GetLogger returns the default logger.
func GetLogger() *Logger {
return defaultLogger
}
// SetLevel updates the log level (thread-safe).
func (l *Logger) SetLevel(level LogLevel) {
l.mu.Lock()
l.level = level
l.mu.Unlock()
}
// SetPromptLogging updates the prompt logging policy.
func (l *Logger) SetPromptLogging(policy string) {
l.mu.Lock()
l.promptLogging = policy
l.mu.Unlock()
}
func (l *Logger) shouldLog(level LogLevel) bool {
l.mu.RLock()
defer l.mu.RUnlock()
return level >= l.level
}
func (l *Logger) maskSensitive(data map[string]any) map[string]any {
if data == nil {
return nil
}
masked := make(map[string]any, len(data))
for k, v := range data {
if l.isSensitive(k) {
masked[k] = "***REDACTED***"
} else if sub, ok := v.(map[string]any); ok {
masked[k] = l.maskSensitive(sub)
} else {
masked[k] = v
}
}
return masked
}
func (l *Logger) isSensitive(key string) bool {
lk := strings.ToLower(key)
for _, s := range l.maskFields {
if strings.Contains(lk, s) {
return true
}
}
return false
}
func (l *Logger) write(entry LogEntry) {
if !l.shouldLog(parseLevelFromString(entry.Level)) {
return
}
if entry.Extra != nil {
entry.Extra = l.maskSensitive(entry.Extra)
}
if entry.Timestamp == "" {
entry.Timestamp = time.Now().UTC().Format(time.RFC3339Nano)
}
data, err := json.Marshal(entry)
if err != nil {
fmt.Fprintf(os.Stderr, "log marshal error: %v\n", err)
return
}
l.mu.Lock()
fmt.Fprintln(l.output, string(data))
l.mu.Unlock()
}
func parseLevelFromString(s string) LogLevel {
switch strings.ToUpper(s) {
case "DEBUG":
return LevelDebug
case "INFO":
return LevelInfo
case "WARN", "WARNING":
return LevelWarn
case "ERROR":
return LevelError
default:
return LevelInfo
}
}
// LogFields is a builder for structured log fields.
type LogFields struct {
fields map[string]any
}
func F() *LogFields {
return &LogFields{fields: make(map[string]any)}
}
func (f *LogFields) Set(key string, value any) *LogFields {
f.fields[key] = value
return f
}
func (f *LogFields) RequestID(id string) *LogFields { f.fields["request_id"] = id; return f }
func (f *LogFields) TaskID(id string) *LogFields { f.fields["task_id"] = id; return f }
func (f *LogFields) SessionID(id string) *LogFields { f.fields["session_id"] = id; return f }
func (f *LogFields) TraceID(id string) *LogFields { f.fields["trace_id"] = id; return f }
func (f *LogFields) Application(app string) *LogFields { f.fields["application"] = app; return f }
func (f *LogFields) TenantID(id string) *LogFields { f.fields["tenant_id"] = id; return f }
func (f *LogFields) UserID(id string) *LogFields { f.fields["user_id"] = id; return f }
func (f *LogFields) Event(e string) *LogFields { f.fields["event"] = e; return f }
func (f *LogFields) Reason(r string) *LogFields { f.fields["reason"] = r; return f }
func (l *Logger) Debug(msg string, fields *LogFields) {
entry := l.buildEntry("DEBUG", msg, fields)
l.write(entry)
}
func (l *Logger) Info(msg string, fields *LogFields) {
entry := l.buildEntry("INFO", msg, fields)
l.write(entry)
}
func (l *Logger) Warn(msg string, fields *LogFields) {
entry := l.buildEntry("WARN", msg, fields)
l.write(entry)
}
func (l *Logger) Error(msg string, fields *LogFields) {
entry := l.buildEntry("ERROR", msg, fields)
l.write(entry)
}
func (l *Logger) buildEntry(level, msg string, fields *LogFields) LogEntry {
entry := LogEntry{
Level: level,
Message: msg,
}
if fields != nil {
for k, v := range fields.fields {
switch k {
case "event":
if s, ok := v.(string); ok {
entry.Event = s
}
case "request_id":
if s, ok := v.(string); ok {
entry.RequestID = s
}
case "task_id":
if s, ok := v.(string); ok {
entry.TaskID = s
}
case "session_id":
if s, ok := v.(string); ok {
entry.SessionID = s
}
case "trace_id":
if s, ok := v.(string); ok {
entry.TraceID = s
}
case "application":
if s, ok := v.(string); ok {
entry.Application = s
}
case "tenant_id":
if s, ok := v.(string); ok {
entry.TenantID = s
}
case "user_id":
if s, ok := v.(string); ok {
entry.UserID = s
}
case "reason":
if s, ok := v.(string); ok {
entry.Reason = s
}
case "from_state":
if s, ok := v.(string); ok {
entry.FromState = s
}
case "to_state":
if s, ok := v.(string); ok {
entry.ToState = s
}
case "logical_model":
if s, ok := v.(string); ok {
entry.LogicalModel = s
}
case "actual_model":
if s, ok := v.(string); ok {
entry.ActualModel = s
}
case "node_id":
if s, ok := v.(string); ok {
entry.NodeID = s
}
case "degraded":
if b, ok := v.(bool); ok {
entry.Degraded = b
}
default:
if entry.Extra == nil {
entry.Extra = make(map[string]any)
}
entry.Extra[k] = v
}
}
}
return entry
}
// SetLogLevel updates the global log level.
func SetLogLevel(level string) {
defaultLogger.SetLevel(ParseLogLevel(level))
}
// SetPromptLoggingPolicy updates the global prompt logging policy.
func SetPromptLoggingPolicy(policy string) {
defaultLogger.SetPromptLogging(policy)
}
+130
View File
@@ -0,0 +1,130 @@
package observability
import (
"bytes"
"encoding/json"
"os"
"strings"
"testing"
)
func TestParseLogLevel(t *testing.T) {
tests := []struct {
input string
want LogLevel
}{
{"debug", LevelDebug}, {"info", LevelInfo},
{"warn", LevelWarn}, {"warning", LevelWarn},
{"error", LevelError}, {"invalid", LevelInfo},
}
for _, tt := range tests {
got := ParseLogLevel(tt.input)
if got != tt.want {
t.Errorf("ParseLogLevel(%q) = %d, want %d", tt.input, got, tt.want)
}
}
}
func TestLoggerMaskSensitive(t *testing.T) {
logger := &Logger{
level: LevelInfo,
maskFields: []string{"api_key", "secret", "password", "token"},
}
data := map[string]any{
"api_key": "sk-12345",
"message": "hello",
"nested": map[string]any{
"secret": "my-secret",
},
}
masked := logger.maskSensitive(data)
if masked["api_key"] != "***REDACTED***" {
t.Errorf("expected api_key redacted, got %v", masked["api_key"])
}
if masked["message"] != "hello" {
t.Errorf("expected message preserved, got %v", masked["message"])
}
nested, ok := masked["nested"].(map[string]any)
if !ok {
t.Fatal("expected nested map")
}
if nested["secret"] != "***REDACTED***" {
t.Errorf("expected nested secret redacted, got %v", nested["secret"])
}
}
func TestLogFieldsBuilder(t *testing.T) {
f := F().RequestID("req-1").TaskID("task-1").Event("test_event").Set("custom", "value")
if f.fields["request_id"] != "req-1" {
t.Error("request_id not set")
}
if f.fields["task_id"] != "task-1" {
t.Error("task_id not set")
}
if f.fields["event"] != "test_event" {
t.Error("event not set")
}
if f.fields["custom"] != "value" {
t.Error("custom not set")
}
}
func TestLoggerWrite(t *testing.T) {
// Use a temp file to capture output
tmpFile, err := os.CreateTemp("", "logtest*.json")
if err != nil {
t.Fatalf("create temp file: %v", err)
}
defer os.Remove(tmpFile.Name())
logger := NewLogger(LevelDebug, tmpFile, "metadata_only")
logger.Info("test message", F().RequestID("req-123").Event("unit_test"))
tmpFile.Close()
data, err := os.ReadFile(tmpFile.Name())
if err != nil {
t.Fatalf("read log file: %v", err)
}
var entry map[string]any
if err := json.Unmarshal(bytes.TrimSpace(data), &entry); err != nil {
t.Fatalf("parse log json: %v\nraw: %s", err, string(data))
}
if entry["level"] != "INFO" {
t.Errorf("expected level INFO, got %v", entry["level"])
}
if entry["message"] != "test message" {
t.Errorf("expected message 'test message', got %v", entry["message"])
}
if entry["request_id"] != "req-123" {
t.Errorf("expected request_id req-123, got %v", entry["request_id"])
}
if entry["event"] != "unit_test" {
t.Errorf("expected event unit_test, got %v", entry["event"])
}
}
func TestLoggerLevelFiltering(t *testing.T) {
// This test verifies that debug messages are not logged when level is INFO
tmpFile, err := os.CreateTemp("", "logtest*.json")
if err != nil {
t.Fatalf("create temp file: %v", err)
}
defer os.Remove(tmpFile.Name())
logger := NewLogger(LevelWarn, tmpFile, "metadata_only")
logger.Info("should not appear", F().Event("info_event"))
logger.Warn("should appear", F().Event("warn_event"))
tmpFile.Close()
data, _ := os.ReadFile(tmpFile.Name())
if strings.Contains(string(data), "should not appear") {
t.Error("INFO message was logged when level is WARN")
}
if !strings.Contains(string(data), "should appear") {
t.Error("WARN message was not logged")
}
}
+178
View File
@@ -0,0 +1,178 @@
package observability
import (
"fmt"
"net/http"
"sync"
"sync/atomic"
)
// Metrics holds all Prometheus-compatible metrics for the gateway.
type Metrics struct {
mu sync.RWMutex
// Counters
requestsTotal map[string]int64 // by status
tasksTotal map[string]int64 // by state
tokensInputTotal int64
tokensOutputTotal int64
cancellationsTotal int64
queueTimeoutsTotal int64
firstTokenTimeoutsTotal int64
inferenceTimeoutsTotal int64
degradedRequestsTotal int64
// Gauges
queueLength int64
runningTasks int64
activeSessions int64
backpressureLevel int64
// Histograms (simplified as buckets)
gatewayLatencyBuckets map[string]int64
firstTokenLatencyBuckets map[string]int64
}
// NewMetrics creates a new Metrics instance.
func NewMetrics() *Metrics {
return &Metrics{
requestsTotal: make(map[string]int64),
tasksTotal: make(map[string]int64),
gatewayLatencyBuckets: make(map[string]int64),
firstTokenLatencyBuckets: make(map[string]int64),
}
}
// IncRequest increments the request counter by status.
func (m *Metrics) IncRequest(status string) {
key := fmt.Sprintf("status=%s", status)
m.mu.Lock()
m.requestsTotal[key]++
m.mu.Unlock()
}
// IncTask increments the task counter by final state.
func (m *Metrics) IncTask(state string) {
key := fmt.Sprintf("state=%s", state)
m.mu.Lock()
m.tasksTotal[key]++
m.mu.Unlock()
}
// AddTokens adds to the token counters.
func (m *Metrics) AddTokens(input, output int) {
atomic.AddInt64(&m.tokensInputTotal, int64(input))
atomic.AddInt64(&m.tokensOutputTotal, int64(output))
}
// IncCancellation increments the cancellation counter.
func (m *Metrics) IncCancellation() {
atomic.AddInt64(&m.cancellationsTotal, 1)
}
// IncQueueTimeout increments the queue timeout counter.
func (m *Metrics) IncQueueTimeout() {
atomic.AddInt64(&m.queueTimeoutsTotal, 1)
}
// IncFirstTokenTimeout increments the first token timeout counter.
func (m *Metrics) IncFirstTokenTimeout() {
atomic.AddInt64(&m.firstTokenTimeoutsTotal, 1)
}
// IncInferenceTimeout increments the inference timeout counter.
func (m *Metrics) IncInferenceTimeout() {
atomic.AddInt64(&m.inferenceTimeoutsTotal, 1)
}
// IncDegraded increments the degraded request counter.
func (m *Metrics) IncDegraded() {
atomic.AddInt64(&m.degradedRequestsTotal, 1)
}
// SetQueueLength sets the current queue length gauge.
func (m *Metrics) SetQueueLength(n int) {
atomic.StoreInt64(&m.queueLength, int64(n))
}
// SetRunningTasks sets the running tasks gauge.
func (m *Metrics) SetRunningTasks(n int) {
atomic.StoreInt64(&m.runningTasks, int64(n))
}
// SetActiveSessions sets the active sessions gauge.
func (m *Metrics) SetActiveSessions(n int) {
atomic.StoreInt64(&m.activeSessions, int64(n))
}
// SetBackpressureLevel sets the backpressure level gauge.
func (m *Metrics) SetBackpressureLevel(level int) {
atomic.StoreInt64(&m.backpressureLevel, int64(level))
}
// ObserveGatewayLatency records gateway latency in a histogram bucket.
func (m *Metrics) ObserveGatewayLatency(ms int64) {
bucket := latencyBucket(ms)
m.mu.Lock()
m.gatewayLatencyBuckets[bucket]++
m.mu.Unlock()
}
// ObserveFirstTokenLatency records first token latency in a histogram bucket.
func (m *Metrics) ObserveFirstTokenLatency(ms int64) {
bucket := latencyBucket(ms)
m.mu.Lock()
m.firstTokenLatencyBuckets[bucket]++
m.mu.Unlock()
}
func latencyBucket(ms int64) string {
buckets := []int64{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000}
for _, b := range buckets {
if ms <= b {
return fmt.Sprintf("le_%d", b)
}
}
return "le_inf"
}
// Handler returns an http.HandlerFunc that writes Prometheus-format metrics.
func (m *Metrics) Handler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
// Counters
m.mu.RLock()
for key, val := range m.requestsTotal {
fmt.Fprintf(w, "edgeai_requests_total{%s} %d\n", key, val)
}
for key, val := range m.tasksTotal {
fmt.Fprintf(w, "edgeai_tasks_total{%s} %d\n", key, val)
}
m.mu.RUnlock()
fmt.Fprintf(w, "edgeai_tokens_input_total %d\n", atomic.LoadInt64(&m.tokensInputTotal))
fmt.Fprintf(w, "edgeai_tokens_output_total %d\n", atomic.LoadInt64(&m.tokensOutputTotal))
fmt.Fprintf(w, "edgeai_cancellations_total %d\n", atomic.LoadInt64(&m.cancellationsTotal))
fmt.Fprintf(w, "edgeai_queue_timeouts_total %d\n", atomic.LoadInt64(&m.queueTimeoutsTotal))
fmt.Fprintf(w, "edgeai_first_token_timeouts_total %d\n", atomic.LoadInt64(&m.firstTokenTimeoutsTotal))
fmt.Fprintf(w, "edgeai_inference_timeouts_total %d\n", atomic.LoadInt64(&m.inferenceTimeoutsTotal))
fmt.Fprintf(w, "edgeai_degraded_requests_total %d\n", atomic.LoadInt64(&m.degradedRequestsTotal))
// Gauges
fmt.Fprintf(w, "edgeai_queue_length %d\n", atomic.LoadInt64(&m.queueLength))
fmt.Fprintf(w, "edgeai_running_tasks %d\n", atomic.LoadInt64(&m.runningTasks))
fmt.Fprintf(w, "edgeai_active_sessions %d\n", atomic.LoadInt64(&m.activeSessions))
fmt.Fprintf(w, "edgeai_backpressure_level %d\n", atomic.LoadInt64(&m.backpressureLevel))
// Histograms
m.mu.RLock()
for bucket, count := range m.gatewayLatencyBuckets {
fmt.Fprintf(w, "edgeai_gateway_latency_bucket{%s} %d\n", bucket, count)
}
for bucket, count := range m.firstTokenLatencyBuckets {
fmt.Fprintf(w, "edgeai_first_token_latency_bucket{%s} %d\n", bucket, count)
}
m.mu.RUnlock()
}
}