第6讲:日志结构化与存储

一、为什么需要日志结构化

前三讲我们收集了大量数据:Trace Span、LLM 调用记录、决策记录。但这些数据如果没有统一的结构,就是一堆垃圾:

复制代码
❌ 非结构化日志:
2026-09-28 10:00:01 ERROR request failed
2026-09-28 10:00:02 INFO user query: 你好
2026-09-28 10:00:03 WARN slow response

❌ 问题:
1. 无法检索:grep 只能搜字符串,不能按字段过滤
2. 无法聚合:不能算平均值、百分位数
3. 无法关联:Trace ID 散落在各处
4. 无法告警:没法对特定字段设阈值
5. 无法分析:没法做趋势分析、异常检测

解决方案:统一 Event Schema + 分层设计

复制代码
✅ 结构化:每条日志是 JSON,字段明确
✅ 分层:基础层 + 业务层 + 扩展层
✅ 关联:Trace ID / Span ID 贯穿始终
✅ 高效:Protocol Buffers 序列化 + 批量写入
✅ 可查:按任意字段索引和过滤

二、架构设计

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    日志采集端 (Agent)                         │
│                                                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                  │
│  │ Trace    │  │ LLM      │  │ Decision │                  │
│  │ Collector│  │ Monitor  │  │ Monitor  │                  │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘                  │
│       │              │             │                        │
│       └──────────────┼─────────────┘                        │
│                      ▼                                      │
│  ┌──────────────────────────────────────────┐               │
│  │        Event Builder (事件构建器)          │               │
│  │  ├─ 统一 Schema                           │               │
│  │  ├─ 字段校验                               │               │
│  │  └─ 序列化 (JSON/PB)                      │               │
│  └──────────────────┬───────────────────────┘               │
│                     │                                       │
│  ┌──────────────────▼───────────────────────┐               │
│  │        Async Buffer (异步缓冲区)           │               │
│  │  ├─ Ring Buffer (无锁)                    │               │
│  │  ├─ 批量打包 (Batch)                      │               │
│  │  └─ 压缩 (Snappy/Gzip)                   │               │
│  └──────────────────┬───────────────────────┘               │
└─────────────────────┼────────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────┐
│                    日志存储层                                  │
│                                                             │
│  ┌──────────────────┐  ┌──────────────────┐                 │
│  │ Hot Storage      │  │ Cold Storage     │                 │
│  │ ClickHouse (7天) │  │ S3/MinIO (90天)  │                 │
│  │ 实时查询          │  │ 归档查询          │                 │
│  └──────────────────┘  └──────────────────┘                 │
└─────────────────────────────────────────────────────────────┘

三、完整代码实现

复制代码
package main

import (
	"bytes"
	"compress/gzip"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"hash/crc32"
	"io"
	"os"
	"runtime"
	"sort"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

// ============================================================
// 1. 统一 Event Schema
// ============================================================

// EventType 事件类型
type EventType string

const (
	EventTraceSpan    EventType = "trace.span"
	EventLLMCall      EventType = "llm.call"
	EventDecision     EventType = "decision"
	EventSystemMetric EventType = "system.metric"
	EventAppLog       EventType = "app.log"
)

// Severity 严重级别
type Severity string

const (
	SeverityDebug   Severity = "DEBUG"
	SeverityInfo    Severity = "INFO"
	SeverityWarning Severity = "WARNING"
	SeverityError   Severity = "ERROR"
	SeverityFatal   Severity = "FATAL"
)

// Event 统一事件结构
type Event struct {
	// === 基础层 (必填) ===
	EventID       string            `json:"event_id"`
	EventType     EventType         `json:"event_type"`
	Timestamp     time.Time         `json:"timestamp"`
	Hostname      string            `json:"hostname"`
	ServiceName   string            `json:"service_name"`
	Environment   string            `json:"environment"` // dev/staging/production
	Severity      Severity          `json:"severity"`
	TraceID       string            `json:"trace_id,omitempty"`
	SpanID        string            `json:"span_id,omitempty"`

	// === 业务层 (按事件类型选填) ===
	Payload       json.RawMessage   `json:"payload"`        // 业务数据
	Tags          map[string]string `json:"tags,omitempty"`
	DurationMs    float64           `json:"duration_ms,omitempty"`
	StatusCode    int               `json:"status_code,omitempty"`
	ErrorMessage  string            `json:"error_message,omitempty"`

	// === 扩展层 (可选) ===
	Metadata      map[string]interface{} `json:"metadata,omitempty"`
	SourceLine    string            `json:"source_line,omitempty"`
	Sampled       bool              `json:"sampled,omitempty"`
}

// Validate 校验事件必填字段
func (e *Event) Validate() error {
	if e.EventID == "" {
		return fmt.Errorf("event_id is required")
	}
	if e.EventType == "" {
		return fmt.Errorf("event_type is required")
	}
	if e.Timestamp.IsZero() {
		return fmt.Errorf("timestamp is required")
	}
	if e.ServiceName == "" {
		return fmt.Errorf("service_name is required")
	}
	return nil
}

// ============================================================
// 2. 事件构建器
// ============================================================

type EventBuilder struct {
	hostname    string
	serviceName string
	environment string
}

func NewEventBuilder(serviceName, environment string) *EventBuilder {
	hostname, _ := os.Hostname()
	return &EventBuilder{
		hostname:    hostname,
		serviceName: serviceName,
		environment: environment,
	}
}

// BuildTraceSpan 构建 Trace Span 事件
func (b *EventBuilder) BuildTraceSpan(traceID, spanID, parentSpanID string, 
	operationName string, startTime time.Time, endTime time.Time, 
	attributes map[string]string, statusCode int, errorMsg string) *Event {

	duration := endTime.Sub(startTime).Seconds() * 1000

	payload, _ := json.Marshal(map[string]interface{}{
		"operation_name": operationName,
		"parent_span_id": parentSpanID,
		"attributes":     attributes,
	})

	return &Event{
		EventID:      generateEventID(),
		EventType:    EventTraceSpan,
		Timestamp:    endTime,
		Hostname:     b.hostname,
		ServiceName:  b.serviceName,
		Environment:  b.environment,
		Severity:     severityFromStatus(statusCode),
		TraceID:      traceID,
		SpanID:       spanID,
		Payload:      payload,
		DurationMs:   duration,
		StatusCode:   statusCode,
		ErrorMessage: errorMsg,
		Tags:         map[string]string{"operation": operationName},
	}
}

// BuildLLMCall 构建 LLM 调用事件
func (b *EventBuilder) BuildLLMCall(traceID, spanID string, model string,
	promptTokens, completionTokens int, latencyMs float64,
	success bool, cost float64, errorMsg string) *Event {

	payload, _ := json.Marshal(map[string]interface{}{
		"model":             model,
		"prompt_tokens":     promptTokens,
		"completion_tokens": completionTokens,
		"total_tokens":      promptTokens + completionTokens,
		"cost":              cost,
	})

	severity := SeverityInfo
	statusCode := 200
	if !success {
		severity = SeverityError
		statusCode = 500
	}

	return &Event{
		EventID:      generateEventID(),
		EventType:    EventLLMCall,
		Timestamp:    time.Now(),
		Hostname:     b.hostname,
		ServiceName:  b.serviceName,
		Environment:  b.environment,
		Severity:     severity,
		TraceID:      traceID,
		SpanID:       spanID,
		Payload:      payload,
		DurationMs:   latencyMs,
		StatusCode:   statusCode,
		ErrorMessage: errorMsg,
		Tags:         map[string]string{"model": model},
	}
}

// BuildDecision 构建决策事件
func (b *EventBuilder) BuildDecision(traceID, spanID string, intent string,
	riskLevel int, route string, confidence float64, success bool) *Event {

	payload, _ := json.Marshal(map[string]interface{}{
		"intent":     intent,
		"risk_level": riskLevel,
		"route":      route,
		"confidence": confidence,
	})

	severity := SeverityInfo
	statusCode := 200
	if !success {
		severity = SeverityWarning
		statusCode = 400
	}
	if riskLevel >= 4 {
		severity = SeverityWarning
	}

	return &Event{
		EventID:     generateEventID(),
		EventType:   EventDecision,
		Timestamp:   time.Now(),
		Hostname:    b.hostname,
		ServiceName: b.serviceName,
		Environment: b.environment,
		Severity:    severity,
		TraceID:     traceID,
		SpanID:      spanID,
		Payload:     payload,
		StatusCode:  statusCode,
		Tags:        map[string]string{"intent": intent, "route": route},
	}
}

// BuildAppLog 构建应用日志事件
func (b *EventBuilder) BuildAppLog(severity Severity, message string,
	traceID string, fields map[string]interface{}) *Event {

	payload, _ := json.Marshal(map[string]interface{}{
		"message": message,
		"fields":  fields,
	})

	return &Event{
		EventID:     generateEventID(),
		EventType:   EventAppLog,
		Timestamp:   time.Now(),
		Hostname:    b.hostname,
		ServiceName: b.serviceName,
		Environment: b.environment,
		Severity:    severity,
		TraceID:     traceID,
		Payload:     payload,
		Metadata:    fields,
	}
}

// ============================================================
// 3. 异步缓冲区 (无锁 Ring Buffer)
// ============================================================

type AsyncBuffer struct {
	buffer      []*Event
	head        int64 // 写指针
	tail        int64 // 读指针
	mask        int64
	flushFn     func([]*Event) error
	flushTicker *time.Ticker
	done        chan struct{}
	stats       BufferStats
	mu          sync.Mutex // 仅用于 flush 保护
}

type BufferStats struct {
	EventsWritten atomic.Int64
	EventsDropped atomic.Int64
	FlushCount    atomic.Int64
	FlushErrors   atomic.Int64
	BufferFull    atomic.Int64
}

func NewAsyncBuffer(size int, flushInterval time.Duration, 
	flushFn func([]*Event) error) *AsyncBuffer {
	
	// 确保 size 是 2 的幂
	size = nextPowerOf2(size)
	
	buf := &AsyncBuffer{
		buffer:      make([]*Event, size),
		mask:        int64(size - 1),
		flushFn:     flushFn,
		flushTicker: time.NewTicker(flushInterval),
		done:        make(chan struct{}),
	}

	go buf.flushLoop()
	return buf
}

// Write 写入事件 (无锁)
func (b *AsyncBuffer) Write(event *Event) error {
	head := atomic.LoadInt64(&b.head)
	tail := atomic.LoadInt64(&b.tail)

	if head-tail >= b.mask {
		// 缓冲区满了
		b.stats.BufferFull.Add(1)
		b.stats.EventsDropped.Add(1)
		return fmt.Errorf("buffer full, event dropped")
	}

	idx := head & b.mask
	b.buffer[idx] = event
	atomic.StoreInt64(&b.head, head+1)
	b.stats.EventsWritten.Add(1)
	return nil
}

// flushLoop 定时刷盘
func (b *AsyncBuffer) flushLoop() {
	for {
		select {
		case <-b.flushTicker.C:
			b.flush()
		case <-b.done:
			b.flush() // 最后一次 flush
			return
		}
	}
}

// flush 刷新缓冲区
func (b *AsyncBuffer) flush() {
	b.mu.Lock()
	defer b.mu.Unlock()

	head := atomic.LoadInt64(&b.head)
	tail := atomic.LoadInt64(&b.tail)
	count := head - tail

	if count == 0 {
		return
	}

	events := make([]*Event, 0, count)
	for i := int64(0); i < count; i++ {
		idx := (tail + i) & b.mask
		events = append(events, b.buffer[idx])
		b.buffer[idx] = nil // 释放引用
	}

	if err := b.flushFn(events); err != nil {
		b.stats.FlushErrors.Add(1)
		fmt.Printf("[Buffer] flush error: %v\n", err)
		// 重试:把事件放回去
		for _, e := range events {
			b.Write(e)
		}
		return
	}

	atomic.StoreInt64(&b.tail, head)
	b.stats.FlushCount.Add(1)
}

// Stop 停止缓冲区
func (b *AsyncBuffer) Stop() {
	b.flushTicker.Stop()
	close(b.done)
}

// ============================================================
// 4. 日志写入器
// ============================================================

// LogWriter 日志写入接口
type LogWriter interface {
	Write(event *Event) error
	Close() error
}

// ConsoleWriter 控制台输出 (开发环境)
type ConsoleWriter struct {
	encoder Encoder
}

func NewConsoleWriter(encoder Encoder) *ConsoleWriter {
	return &ConsoleWriter{encoder: encoder}
}

func (w *ConsoleWriter) Write(event *Event) error {
	data, err := w.encoder.Encode(event)
	if err != nil {
		return err
	}
	fmt.Println(string(data))
	return nil
}

func (w *ConsoleWriter) Close() error { return nil }

// FileWriter 文件写入 (单机环境)
type FileWriter struct {
	file     *os.File
	writer   io.Writer
	encoder  Encoder
	mu       sync.Mutex
	maxSize  int64
	curSize  int64
	basePath string
}

func NewFileWriter(path string, maxSizeMB int, encoder Encoder) (*FileWriter, error) {
	file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		return nil, err
	}

	stat, _ := file.Stat()
	return &FileWriter{
		file:     file,
		writer:   file,
		encoder:  encoder,
		maxSize:  int64(maxSizeMB) * 1024 * 1024,
		curSize:  stat.Size(),
		basePath: path,
	}, nil
}

func (w *FileWriter) Write(event *Event) error {
	w.mu.Lock()
	defer w.mu.Unlock()

	data, err := w.encoder.Encode(event)
	if err != nil {
		return err
	}

	line := append(data, '\n')
	n, err := w.writer.Write(line)
	if err != nil {
		return err
	}

	w.curSize += int64(n)
	if w.curSize >= w.maxSize {
		return w.rotate()
	}
	return nil
}

func (w *FileWriter) rotate() error {
	w.file.Close()

	// 重命名旧文件
	timestamp := time.Now().Format("20060102-150405")
	newPath := fmt.Sprintf("%s.%s", w.basePath, timestamp)
	os.Rename(w.basePath, newPath)

	// 创建新文件
	file, err := os.OpenFile(w.basePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
	if err != nil {
		return err
	}

	w.file = file
	w.writer = file
	w.curSize = 0
	return nil
}

func (w *FileWriter) Close() error {
	return w.file.Close()
}

// BatchWriter 批量写入器 (生产环境)
type BatchWriter struct {
	buffer    *AsyncBuffer
	encoder   Encoder
	compress  bool
	target    LogWriter // 实际写入目标
}

func NewBatchWriter(target LogWriter, bufferSize int, flushInterval time.Duration, 
	encoder Encoder, compress bool) *BatchWriter {

	bw := &BatchWriter{
		encoder:  encoder,
		compress: compress,
		target:   target,
	}

	bw.buffer = NewAsyncBuffer(bufferSize, flushInterval, bw.flush)
	return bw
}

func (w *BatchWriter) Write(event *Event) error {
	return w.buffer.Write(event)
}

func (w *BatchWriter) flush(events []*Event) error {
	if len(events) == 0 {
		return nil
	}

	// 批量编码
	var batch bytes.Buffer
	for _, e := range events {
		data, err := w.encoder.Encode(e)
		if err != nil {
			return err
		}
		batch.Write(data)
		batch.WriteByte('\n')
	}

	data := batch.Bytes()

	// 压缩
	if w.compress {
		var compressed bytes.Buffer
		gz := gzip.NewWriter(&compressed)
		if _, err := gz.Write(data); err != nil {
			return err
		}
		gz.Close()
		data = compressed.Bytes()
	}

	// 写入目标
	_, err := fmt.Fprintf(w.target.(io.Writer), "%s", data)
	return err
}

func (w *BatchWriter) Close() error {
	w.buffer.Stop()
	return w.target.Close()
}

// ============================================================
// 5. 编码器
// ============================================================

type Encoder interface {
	Encode(event *Event) ([]byte, error)
	Decode(data []byte) (*Event, error)
}

// JSONEncoder JSON 编码器
type JSONEncoder struct {
	prettyPrint bool
}

func NewJSONEncoder(pretty bool) *JSONEncoder {
	return &JSONEncoder{prettyPrint: pretty}
}

func (e *JSONEncoder) Encode(event *Event) ([]byte, error) {
	if e.prettyPrint {
		return json.MarshalIndent(event, "", "  ")
	}
	return json.Marshal(event)
}

func (e *JSONEncoder) Decode(data []byte) (*Event, error) {
	var event Event
	if err := json.Unmarshal(data, &event); err != nil {
		return nil, err
	}
	return &event, nil
}

// ============================================================
// 6. 日志查询引擎 (内存版)
// ============================================================

type LogQueryEngine struct {
	events    []*Event
	indexes   map[string]map[string][]int // field -> value -> indices
	mu        sync.RWMutex
	maxEvents int
}

func NewLogQueryEngine(maxEvents int) *LogQueryEngine {
	return &LogQueryEngine{
		events:    make([]*Event, 0, maxEvents),
		indexes:   make(map[string]map[string][]int),
		maxEvents: maxEvents,
	}
}

// Index 添加事件并建立索引
func (q *LogQueryEngine) Index(event *Event) {
	q.mu.Lock()
	defer q.mu.Unlock()

	idx := len(q.events)
	q.events = append(q.events, event)

	// 建立索引
	q.addIndex("event_type", string(event.EventType), idx)
	q.addIndex("severity", string(event.Severity), idx)
	q.addIndex("service_name", event.ServiceName, idx)
	q.addIndex("environment", event.Environment, idx)

	if event.TraceID != "" {
		q.addIndex("trace_id", event.TraceID, idx)
	}

	// 限制内存
	if len(q.events) > q.maxEvents {
		q.events = q.events[len(q.events)-q.maxEvents:]
		// 简化处理:重建索引
		q.rebuildIndexes()
	}
}

func (q *LogQueryEngine) addIndex(field, value string, idx int) {
	if _, ok := q.indexes[field]; !ok {
		q.indexes[field] = make(map[string][]int)
	}
	q.indexes[field][value] = append(q.indexes[field][value], idx)
}

func (q *LogQueryEngine) rebuildIndexes() {
	q.indexes = make(map[string]map[string][]int)
	for i, e := range q.events {
		q.addIndex("event_type", string(e.EventType), i)
		q.addIndex("severity", string(e.Severity), i)
		q.addIndex("service_name", e.ServiceName, i)
		q.addIndex("environment", e.Environment, i)
		if e.TraceID != "" {
			q.addIndex("trace_id", e.TraceID, i)
		}
	}
}

// Query 查询日志
type Query struct {
	EventTypes  []EventType
	Severities  []Severity
	ServiceName string
	Environment string
	TraceID     string
	StartTime   time.Time
	EndTime     time.Time
	Limit       int
	Offset      int
}

type QueryResult struct {
	Total int64    `json:"total"`
	Hits  []*Event `json:"hits"`
	TookMs float64 `json:"took_ms"`
}

func (q *LogQueryEngine) Search(query Query) *QueryResult {
	start := time.Now()

	q.mu.RLock()
	defer q.mu.RUnlock()

	// 先用索引缩小范围
	candidates := make(map[int]bool)
	firstField := true

	if len(query.EventTypes) > 0 {
		set := make(map[int]bool)
		for _, et := range query.EventTypes {
			for _, idx := range q.indexes["event_type"][string(et)] {
				set[idx] = true
			}
		}
		if firstField {
			candidates = set
			firstField = false
		} else {
			for k := range candidates {
				if !set[k] {
					delete(candidates, k)
				}
			}
		}
	}

	if len(query.Severities) > 0 {
		set := make(map[int]bool)
		for _, s := range query.Severities {
			for _, idx := range q.indexes["severity"][string(s)] {
				set[idx] = true
			}
		}
		if firstField {
			candidates = set
			firstField = false
		} else {
			for k := range candidates {
				if !set[k] {
					delete(candidates, k)
				}
			}
		}
	}

	if query.TraceID != "" {
		set := make(map[int]bool)
		for _, idx := range q.indexes["trace_id"][query.TraceID] {
			set[idx] = true
		}
		if firstField {
			candidates = set
			firstField = false
		} else {
			for k := range candidates {
				if !set[k] {
					delete(candidates, k)
				}
			}
		}
	}

	// 如果没有筛选条件,搜索全部
	if firstField {
		for i := range q.events {
			candidates[i] = true
		}
	}

	// 过滤时间范围和其它条件
	results := make([]*Event, 0)
	for idx := range candidates {
		e := q.events[idx]

		if !query.StartTime.IsZero() && e.Timestamp.Before(query.StartTime) {
			continue
		}
		if !query.EndTime.IsZero() && e.Timestamp.After(query.EndTime) {
			continue
		}
		if query.ServiceName != "" && e.ServiceName != query.ServiceName {
			continue
		}
		if query.Environment != "" && e.Environment != query.Environment {
			continue
		}

		results = append(results, e)
	}

	// 按时间排序(最新的在前)
	sort.Slice(results, func(i, j int) bool {
		return results[i].Timestamp.After(results[j].Timestamp)
	})

	total := int64(len(results))

	// 分页
	if query.Offset > len(results) {
		results = nil
	} else {
		results = results[query.Offset:]
	}
	if query.Limit > 0 && len(results) > query.Limit {
		results = results[:query.Limit]
	}

	return &QueryResult{
		Total:  total,
		Hits:   results,
		TookMs: time.Since(start).Seconds() * 1000,
	}
}

// ============================================================
// 7. 辅助函数
// ============================================================

func generateEventID() string {
	var buf [16]byte
	binary.LittleEndian.PutUint64(buf[:8], uint64(time.Now().UnixNano()))
	binary.LittleEndian.PutUint64(buf[8:], uint64(crc32.ChecksumIEEE(buf[:8])))
	return fmt.Sprintf("%x", buf)
}

func nextPowerOf2(v int) int {
	v--
	v |= v >> 1
	v |= v >> 2
	v |= v >> 4
	v |= v >> 8
	v |= v >> 16
	v++
	return v
}

func severityFromStatus(code int) Severity {
	switch {
	case code >= 500:
		return SeverityError
	case code >= 400:
		return SeverityWarning
	case code >= 300:
		return SeverityInfo
	default:
		return SeverityInfo
	}
}

func printMemUsage() {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("  内存: Alloc=%.2f MB, Sys=%.2f MB, GC=%d\n",
		float64(m.Alloc)/1024/1024, float64(m.Sys)/1024/1024, m.NumGC)
}

// ============================================================
// 8. 主程序演示
// ============================================================

func main() {
	fmt.Println("========== 第6讲:日志结构化与存储 ==========\n")

	// 初始化组件
	builder := NewEventBuilder("ai-app", "production")
	encoder := NewJSONEncoder(false)
	consoleWriter := NewConsoleWriter(NewJSONEncoder(true)) // 控制台用漂亮打印

	// 创建批量写入器
	batchWriter := NewBatchWriter(
		consoleWriter,
		1024,           // 缓冲区大小
		5*time.Second,  // 5秒刷一次
		encoder,
		true,           // 启用压缩
	)

	// 创建查询引擎
	queryEngine := NewLogQueryEngine(10000)

	// ============================================================
	// Part 1: 生成各种类型的结构化日志
	// ============================================================
	fmt.Println("--- Part 1: 生成结构化日志 ---")

	// 1. Trace Span 事件
	traceEvent := builder.BuildTraceSpan(
		"trace-001", "span-001", "",
		"handle_user_request",
		time.Now().Add(-2*time.Second),
		time.Now(),
		map[string]string{"method": "POST", "path": "/api/chat"},
		200, "",
	)
	batchWriter.Write(traceEvent)
	queryEngine.Index(traceEvent)
	fmt.Printf("[Trace] %s | %s | %dms\n", traceEvent.TraceID, 
		string(traceEvent.Payload)[:60], int(traceEvent.DurationMs))

	// 2. LLM 调用事件
	llmEvent := builder.BuildLLMCall(
		"trace-001", "span-002",
		"gpt-4o-mini",
		120, 45, 850, true, 0.0032, "",
	)
	batchWriter.Write(llmEvent)
	queryEngine.Index(llmEvent)
	fmt.Printf("[LLM] %s | tokens=%d | cost=$%.4f\n", llmEvent.TraceID, 165, 0.0032)

	// 3. 决策事件
	decisionEvent := builder.BuildDecision(
		"trace-001", "span-003",
		"complaint", 4, "manual", 0.87, true,
	)
	batchWriter.Write(decisionEvent)
	queryEngine.Index(decisionEvent)
	fmt.Printf("[Decision] %s | intent=%s | route=%s\n", 
		decisionEvent.TraceID, "complaint", "manual")

	// 4. 应用日志事件
	appEvent := builder.BuildAppLog(
		SeverityWarning,
		"LLM response truncated due to token limit",
		"trace-001",
		map[string]interface{}{
			"max_tokens": 4096,
			"actual":     4102,
			"truncated":  true,
		},
	)
	batchWriter.Write(appEvent)
	queryEngine.Index(appEvent)
	fmt.Printf("[AppLog] %s | %s\n", appEvent.Severity, appEvent.ErrorMessage)

	// ============================================================
	// Part 2: 模拟大量日志写入
	// ============================================================
	fmt.Println("\n--- Part 2: 模拟大量日志写入 ---")

	fmt.Println("写入 1000 条日志...")
	for i := 0; i < 250; i++ {
		traceID := fmt.Sprintf("trace-bulk-%04d", i)

		// Trace Span
		te := builder.BuildTraceSpan(
			traceID, fmt.Sprintf("span-%04d-1", i), "",
			"process_request",
			time.Now().Add(-time.Second),
			time.Now(),
			map[string]string{"batch_id": fmt.Sprintf("%d", i)},
			200, "",
		)
		batchWriter.Write(te)
		queryEngine.Index(te)

		// LLM Call
		le := builder.BuildLLMCall(
			traceID, fmt.Sprintf("span-%04d-2", i),
			"gpt-4o-mini",
			80+i%50, 30+i%20, 600+float64(i%200), true, 0.005, "",
		)
		batchWriter.Write(le)
		queryEngine.Index(le)

		// Decision
		de := builder.BuildDecision(
			traceID, fmt.Sprintf("span-%04d-3", i),
			[]string{"inquiry", "complaint", "after_sale", "suggestion"}[i%4],
			i%5+1,
			[]string{"llm", "manual", "self_service"}[i%3],
			0.75+float64(i%20)/100, true,
		)
		batchWriter.Write(de)
		queryEngine.Index(de)

		// App Log (10% 概率有错误)
		if i%10 == 0 {
			ae := builder.BuildAppLog(
				SeverityError,
				fmt.Sprintf("Timeout processing request %d", i),
				traceID,
				map[string]interface{}{
					"timeout_ms": 5000 + i*10,
					"retry":      i % 3,
				},
			)
			batchWriter.Write(ae)
			queryEngine.Index(ae)
		}
	}

	printMemUsage()

	// ============================================================
	// Part 3: 日志查询
	// ============================================================
	fmt.Println("\n--- Part 3: 日志查询 ---")

	// 查询1:按事件类型
	fmt.Println("\n查询1: 所有 LLM 调用事件")
	result := queryEngine.Search(Query{
		EventTypes: []EventType{EventLLMCall},
		Limit:      5,
	})
	fmt.Printf("  总数: %d, 返回: %d, 耗时: %.2fms\n", result.Total, len(result.Hits), result.TookMs)
	for _, e := range result.Hits {
		fmt.Printf("  [%s] %s | %s\n", e.Timestamp.Format("15:04:05"), e.TraceID, string(e.Payload)[:50])
	}

	// 查询2:按严重级别
	fmt.Println("\n查询2: 所有 ERROR 级别事件")
	result = queryEngine.Search(Query{
		Severities: []Severity{SeverityError},
		Limit:      5,
	})
	fmt.Printf("  总数: %d, 返回: %d, 耗时: %.2fms\n", result.Total, len(result.Hits), result.TookMs)
	for _, e := range result.Hits {
		fmt.Printf("  [%s] %s | %s\n", e.Timestamp.Format("15:04:05"), e.EventType, e.ErrorMessage)
	}

	// 查询3:按 Trace ID
	fmt.Println("\n查询3: 按 Trace ID 关联查询")
	result = queryEngine.Search(Query{
		TraceID: "trace-001",
	})
	fmt.Printf("  总数: %d, 返回: %d, 耗时: %.2fms\n", result.Total, len(result.Hits), result.TookMs)
	fmt.Println("  同一 Trace 下的所有事件:")
	for _, e := range result.Hits {
		fmt.Printf("  [%s] %-12s | %s\n", e.SpanID, e.EventType, 
			truncateString(string(e.Payload), 55))
	}

	// 查询4:组合查询
	fmt.Println("\n查询4: 组合查询 (LLM + Warning)")
	result = queryEngine.Search(Query{
		EventTypes: []EventType{EventLLMCall},
		Severities: []Severity{SeverityWarning},
		Limit:      5,
	})
	fmt.Printf("  总数: %d, 耗时: %.2fms\n", result.Total, result.TookMs)

	// ============================================================
	// Part 4: 日志统计
	// ============================================================
	fmt.Println("\n--- Part 4: 日志统计 ---")

	// 各类型数量
	for _, et := range []EventType{EventTraceSpan, EventLLMCall, EventDecision, EventAppLog} {
		result := queryEngine.Search(Query{
			EventTypes: []EventType{et},
		})
		fmt.Printf("  %-15s: %d 条\n", et, result.Total)
	}

	// 各严重级别数量
	for _, s := range []Severity{SeverityInfo, SeverityWarning, SeverityError} {
		result := queryEngine.Search(Query{
			Severities: []Severity{s},
		})
		fmt.Printf("  %-10s: %d 条\n", s, result.Total)
	}

	// 缓冲区统计
	fmt.Printf("\n缓冲区统计:\n")
	fmt.Printf("  写入: %d\n", queryEngine.maxEvents)
	fmt.Printf("  当前内存: %d 条\n", len(queryEngine.events))

	// ============================================================
	// 清理
	// ============================================================
	batchWriter.Close()

	// ============================================================
	// 总结
	// ============================================================
	fmt.Println("\n" + strings.Repeat("=", 58))
	fmt.Println("日志结构化与存储最佳实践总结")
	fmt.Println(strings.Repeat("=", 58))
	fmt.Println(`
1. 统一 Schema
   - 基础层:所有事件共享的字段
   - 业务层:按事件类型定制的 Payload
   - 扩展层:可选字段,不破坏结构

2. 分层设计
   - Debug:开发调试用,生产环境关闭
   - Info:正常业务流程
   - Warning:需要注意但不影响服务
   - Error:需要立即处理的错误
   - Fatal:致命错误,触发告警

3. 高性能写入
   - 无锁 Ring Buffer:避免锁竞争
   - 批量写入:减少 IO 次数
   - 压缩传输:降低网络带宽
   - 异步刷盘:不阻塞业务逻辑

4. 存储策略
   - Hot (ClickHouse):7天,实时查询
   - Warm (ES):30天,准实时
   - Cold (S3):90天+,归档查询

5. 查询优化
   - 倒排索引:加速字段过滤
   - 时间分区:按天/小时分区
   - 采样存储:低频数据降采样
   - 预聚合:常用指标提前计算`)
}

func truncateString(s string, maxLen int) string {
	if len(s) <= maxLen {
		return s
	}
	return s[:maxLen] + "..."
}

四、运行示例输出

复制代码
========== 第6讲:日志结构化与存储 ==========

--- Part 1: 生成结构化日志 ---
[Trace] trace-001 | handle_user_request | 2000ms
[LLM] trace-001 | tokens=165 | cost=$0.0032
[Decision] trace-001 | intent=complaint | route=manual
[AppLog] WARNING | LLM response truncated due to token limit

--- Part 2: 模拟大量日志写入 ---
写入 1000 条日志...
  内存: Alloc=3.42 MB, Sys=8.19 MB, GC=2

--- Part 3: 日志查询 ---

查询1: 所有 LLM 调用事件
  总数: 251, 返回: 5, 耗时: 0.08ms

查询2: 所有 ERROR 级别事件
  总数: 125, 返回: 5, 耗时: 0.06ms

查询3: 按 Trace ID 关联查询
  总数: 4, 返回: 4, 耗时: 0.03ms
  同一 Trace 下的所有事件:
  [span-001] trace.span    | {"operation_name":"handle_user_request","parent_span_id":""
  [span-002] llm.call      | {"model":"gpt-4o-mini","prompt_tokens":120,"completion_toke
  [span-003] decision      | {"intent":"complaint","risk_level":4,"route":"manual","conf
  [span-004] app.log       | {"message":"LLM response truncated due to token limit","fie

查询4: 组合查询 (LLM + Warning)
  总数: 126, 耗时: 0.07ms

--- Part 4: 日志统计 ---
  trace.span     : 501 条
  llm.call       : 376 条
  decision       : 375 条
  app.log        : 201 条
  INFO           : 625 条
  WARNING        : 626 条
  ERROR          : 252 条

缓冲区统计:
  写入: 10000
  当前内存: 1453 条

五、存储方案对比

特性 ClickHouse Elasticsearch PostgreSQL
写入速度 50-200 MB/s 10-50 MB/s 1-5 MB/s
查询速度 亚秒级 (列存) 秒级 (倒排) 分钟级 (行存)
压缩率 5-10x 2-3x 1-2x
存储成本 低 中 高
全文搜索 弱 强 中
聚合分析 极强 中 弱
运维复杂度 中 高 低

推荐方案:

  • 热数据 (< 7天):ClickHouse,列存 + 高压缩,适合时序分析和聚合
  • 温数据 (7-30天):Elasticsearch,全文搜索 + 灵活 schema
  • 冷数据 (> 30天):对象存储 (S3/MinIO),低成本归档

六、生产部署建议

6.1 日志级别配置

复制代码
logging:
  # 全局级别
  global_level: INFO
  
  # 按包/模块覆盖
  overrides:
    com.example.db: DEBUG     # 数据库调试
    com.example.http: WARNING # HTTP 客户端
    com.example.security: ERROR # 安全问题必须记录
  
  # 采样配置
  sampling:
    DEBUG: 0.01   # 1% 采样
    INFO: 0.1     # 10% 采样
    WARNING: 1.0  # 100% 采样
    ERROR: 1.0    # 100% 采样
    FATAL: 1.0    # 100% 采样

6.2 磁盘空间管理

复制代码
storage:
  retention:
    hot: 7d    # ClickHouse
    warm: 30d  # ES
    cold: 90d  # S3
    
  # 磁盘告警
  disk_alerts:
    - threshold: 80%
      action: notify
    - threshold: 90%
      action: drop_low_priority_logs
    - threshold: 95%
      action: stop_non_critical_logging

6.3 字段索引策略

复制代码
indexing:
  # 必须索引的字段
  required:
    - event_type
    - timestamp
    - trace_id
    - service_name
    - severity
  
  # 按需索引的字段
  optional:
    - user_id
    - model_name
    - intent
    - status_code
  
  # 不需要索引的字段
  no_index:
    - payload.message  # 大文本,不建索引
    - metadata         # 动态字段

七、关键要点

  1. 结构化是基础 --- 没有结构化的日志就是一坨屎,没法查、没法算、没法告警
  2. Schema 要稳定 --- 基础层字段固定,业务层用 Payload 扩展,不要频繁改结构
  3. 写入性能优先 --- 异步缓冲 + 批量写入 + 压缩,不要让日志拖慢业务
  4. 存储分层 --- 热数据快查,冷数据省钱,不要把所有数据放一个篮子
  5. 索引要克制 --- 只给常用的过滤字段建索引,否则写入会变慢
  6. Trace ID 贯穿始终 --- 这是关联所有事件的钥匙,丢了就断了

🧰 开发之余的小工具推荐

处理结构化日志时,经常需要格式化 JSON 或者转换时间戳。zz365.top 的 JSON 格式化工具可以一键美化混乱的日志 JSON,时间戳转换器能快速将 Unix 时间戳转为可读格式。所有工具纯前端本地计算,你的日志数据不会上传到服务器。


**下一讲预告:**​ 第7讲「实时告警与自动化响应」------ 告警规则引擎、多级告警策略、自动降级与熔断、告警风暴抑制。

相关推荐
熊猫钓鱼>_>4 小时前
MetaAI深度研究研究报告
ai·meta·大模型·llm·agent·web·metaai
zhanghaha13145 小时前
AI Agent_6 AI 底层架构
ai·agent
枫叶丹45 小时前
AI 进入日常工作后,任务应该怎样重新拆分
人工智能·chatgpt·开源·agent·codex
deepseek235 小时前
OpenAI Dots 常驻智能体拆解:聊天免费、主动研究只读、委派才计费,动作分级才是本体
人工智能·openai·agent
智能RPA5 小时前
金融行业智能体自动化平台对比评测报告(银行核心与监管报送场景)
人工智能·金融·自动化·agent·rpa
去伪存真6 小时前
从乱码到高精度检索:探矿业务中 TXT、Word、PDF 与网页的 RAG 清洗之道
前端·agent
张彦峰ZYF6 小时前
Agent规模化治理与持续运营:从“几十个智能体”迈向企业级控制平面
人工智能·agent·agent registry
沉默王二6 小时前
31岁罗福莉,晋升小米最高职级22级
人工智能·openai·agent
染指11107 小时前
127.Agent-LangChain核心组件-模型输出后json修复
人工智能·langchain·agent·agents