CP定位,技术栈,架构,项目结构,基础框架搭建,开发部署及常见问题

复制代码

# Loki MCP Server - CLAUDE.md

> Go 实现的 MCP Server,集成 Grafana Loki 日志查询。支持 Claude Desktop / Claude Code / Cursor 等客户端通过自然语言查询日志。

---

## 一、项目概述

### 定位

将 Grafana Loki 的日志查询能力通过 MCP (Model Context Protocol) 暴露给 AI 助手,运维人员可以用自然语言代替 LogQL 查询日志。

### 技术栈

| 层 | 技术 | 版本 |
|---|------|------|
| 语言 | Go | 1.24+ |
| MCP SDK | github.com/mark3labs/mcp-go | v0.32.0 |
| 日志存储 | Grafana Loki | 2.9.0 |
| 日志采集 | Promtail | 2.9.0 |
| 可视化 | Grafana | latest |
| 容器化 | Docker + Compose | - |

### 核心能力(3 个 MCP Tool)

| Tool | 用途 | Loki API |
|------|------|----------|
| `loki_query` | 执行 LogQL 查询 | `/loki/api/v1/query_range` |
| `loki_label_names` | 获取所有标签名 | `/loki/api/v1/labels` |
| `loki_label_values` | 获取标签值列表 | `/loki/api/v1/label/{name}/values` |

---

## 二、架构设计

### 整体架构

```
┌─────────────────┐      ┌──────────────────┐      ┌──────────────┐
│  AI Client      │      │  Loki MCP Server │      │  Grafana     │
│  (Claude Code/  │─────→│  :8080           │─────→│  Loki        │
│   Desktop/      │ MCP  │                  │ HTTP │  :3100       │
│   Cursor)       │      │  3 种传输协议:    │      │              │
└─────────────────┘      │  - stdio         │      └──────────────┘
                         │  - SSE (/sse)    │
                         │  - HTTP (/stream)│
                         │                  │
                         │  /healthz (K8s)  │
                         └──────────────────┘
```

### 三种传输协议(同端口共存)

| 协议 | 端点 | 场景 |
|------|------|------|
| stdio | 标准输入输出 | 本地 binary / Docker 集成,Claude Desktop 直接启动进程 |
| SSE | `/sse` + `/mcp` | Server-Sent Events,远程连接(旧协议) |
| Streamable HTTP | `/stream` | 新一代 MCP 远程协议(推荐) |

**设计要点**:通过 `http.ServeMux` 将 SSE 和 Streamable HTTP 注册到同一端口,stdio 作为后台 goroutine 并行运行。

### 项目结构

```
loki-mcp/
├── cmd/
│   ├── server/main.go          # 入口:注册 Tool + 启动 3 种传输
│   └── client/main.go          # JSON-RPC 测试客户端
├── internal/
│   └── handlers/
│       ├── loki.go             # 核心:3 个 Tool 的完整实现 (993 行)
│       └── loki_test.go        # 单元测试 (261 行)
├── pkg/
│   └── utils/logger.go         # 简易日志工具
├── grafana/
│   └── provisioning/datasources/loki.yaml  # Grafana 数据源预配置
├── promtail/
│   └── config.yml              # 日志采集配置
├── examples/
│   ├── claude-desktop/         # 4 种 Claude Desktop 配置示例
│   ├── claude-code-commands/   # Slash Command 模板
│   ├── simple-sse-client.html  # SSE 测试页面
│   └── sse-client.html         # 完整 SSE 客户端
├── docker-compose.yml          # 本地 5 服务开发环境
├── Dockerfile                  # 多阶段构建
├── Makefile                    # 构建/测试/运行
├── go.mod / go.sum
├── run-mcp-server.sh           # 启动脚本
├── test-loki-query.sh          # 查询测试脚本
├── insert-loki-logs.sh         # 插入测试日志
└── README.md
```

---

## 三、从零实现指南

### Phase 1:基础框架搭建

#### 1.1 初始化 Go 项目

```bash
mkdir loki-mcp && cd loki-mcp
go mod init github.com/yourname/loki-mcp
go get github.com/mark3labs/mcp-go@v0.32.0
```

#### 1.2 理解 MCP Server 入口 (`cmd/server/main.go`)

核心模式:**创建 Server → 注册 Tool → 启动传输层**

```go
// 1. 创建 MCP Server 实例
s := server.NewMCPServer(
    "Loki MCP Server", "0.1.0",
    server.WithResourceCapabilities(true, true),
    server.WithLogging(),
)

// 2. 注册工具(Tool 定义 + Handler 函数)
lokiQueryTool := handlers.NewLokiQueryTool()
s.AddTool(lokiQueryTool, handlers.HandleLokiQuery)

// 3. 创建传输层
sseServer := server.NewSSEServer(s,
    server.WithSSEEndpoint("/sse"),
    server.WithMessageEndpoint("/mcp"),
)
streamableServer := server.NewStreamableHTTPServer(s)

// 4. 统一路由
mux := http.NewServeMux()
mux.Handle("/sse", sseServer)
mux.Handle("/mcp", sseServer)
mux.Handle("/stream", streamableServer)
mux.HandleFunc("/healthz", healthHandler)

// 5. 并行启动 HTTP + stdio
go http.ListenAndServe(":8080", mux)
go server.ServeStdio(s)

// 6. 优雅关闭
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
```

**关键设计决策**:
- 三种传输协议同端口:简化部署,一个端口搞定所有客户端
- stdio 后台运行:兼容 Claude Desktop 的进程模式
- `/healthz` 端点:适配 K8s readiness/liveness probe

#### 1.3 理解 Tool 定义模式

每个 Tool 由两部分组成:
1. **Tool 定义函数** (`NewXxxTool()`) — 声明参数 schema
2. **Handler 函数** (`HandleXxx()`) — 处理请求逻辑

```go
// Tool 定义:声明参数、类型、默认值、描述
func NewLokiQueryTool() mcp.Tool {
    return mcp.NewTool("loki_query",
        mcp.WithDescription("Run a query against Grafana Loki"),
        mcp.WithString("query", mcp.Required(), mcp.Description("LogQL query string")),
        mcp.WithString("url", mcp.Description("Loki server URL"), mcp.DefaultString(lokiURL)),
        mcp.WithString("start", mcp.Description("Start time (default: 1h ago)")),
        mcp.WithNumber("limit", mcp.Description("Max entries (default: 100)")),
        mcp.WithString("format", mcp.DefaultString("raw")),
        // ... 认证参数
    )
}

// Handler:提取参数 → 构建请求 → 调用 Loki API → 格式化输出
func HandleLokiQuery(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    args := request.GetArguments()
    // ... 处理逻辑
    return mcp.NewToolResultText(formattedResult), nil
}
```

### Phase 2:核心逻辑实现

#### 2.1 请求处理流程 (`internal/handlers/loki.go`)

```
参数提取 → 环境变量回退 → 时间解析 → URL构建 → HTTP请求 → 响应解析 → 格式化输出
```

#### 2.2 参数提取模式(带环境变量回退)

每个参数都遵循:**请求参数 > 环境变量 > 默认值** 的优先级链

```go
// 统一模式:先看请求参数,再看环境变量
var lokiURL string
if urlArg, ok := args["url"].(string); ok && urlArg != "" {
    lokiURL = urlArg
} else {
    lokiURL = os.Getenv("LOKI_URL")
    if lokiURL == "" {
        lokiURL = "http://localhost:3100"
    }
}
```

环境变量清单:

| 变量 | 用途 | 默认值 |
|------|------|--------|
| `LOKI_URL` | Loki 地址 | `http://localhost:3100` |
| `LOKI_ORG_ID` | 租户 ID | 空 |
| `LOKI_USERNAME` | Basic Auth 用户名 | 空 |
| `LOKI_PASSWORD` | Basic Auth 密码 | 空 |
| `LOKI_TOKEN` | Bearer Token | 空 |
| `PORT` | 服务端口 | `8080` |

#### 2.3 时间解析 (`parseTime`)

支持多种输入格式,按顺序尝试:

```go
func parseTime(timeStr string) (time.Time, error) {
    // 1. "now" 关键字
    if timeStr == "now" { return time.Now(), nil }

    // 2. 相对时间: "-1h", "-30m"
    if timeStr[0] == '-' {
        duration, err := time.ParseDuration(timeStr)
        if err == nil { return time.Now().Add(duration), nil }
    }

    // 3. RFC3339: "2024-01-15T10:30:45Z"
    // 4. ISO 变体: "2006-01-02T15:04:05", "2006-01-02 15:04:05"
    // 5. 纯日期: "2006-01-02"
}
```

#### 2.4 URL 构建

智能路径拼接,处理各种 base URL 格式:

```go
func buildLokiQueryURL(baseURL, query string, start, end int64, limit int) (string, error) {
    u, _ := url.Parse(baseURL)

    // 路径规范化:避免重复拼接
    if !strings.Contains(u.Path, "loki/api/v1") {
        u.Path = "/loki/api/v1/query_range"
    }

    // 查询参数
    q := u.Query()
    q.Set("query", query)           // LogQL
    q.Set("start", fmt.Sprintf("%d", start))  // Unix 秒
    q.Set("end", fmt.Sprintf("%d", end))
    q.Set("limit", fmt.Sprintf("%d", limit))
    u.RawQuery = q.Encode()
    return u.String(), nil
}
```

#### 2.5 认证机制

三级认证,优先级:Bearer Token > Basic Auth > 无认证

```go
if token != "" {
    req.Header.Add("Authorization", "Bearer "+token)
} else if username != "" || password != "" {
    req.SetBasicAuth(username, password)
}

// 多租户隔离(始终添加如果有值)
if orgID != "" {
    req.Header.Add("X-Scope-OrgID", orgID)
}
```

#### 2.6 响应数据结构

```go
type LokiResult struct {
    Status string   `json:"status"`   // "success" | "error"
    Data   LokiData `json:"data"`
    Error  string   `json:"error,omitempty"`
}

type LokiData struct {
    ResultType string      `json:"resultType"` // "streams"
    Result     []LokiEntry `json:"result"`
}

type LokiEntry struct {
    Stream map[string]string `json:"stream"` // 标签: {job: "xx", pod: "xx"}
    Values [][]string        `json:"values"` // [[纳秒时间戳, 日志行], ...]
}
```

#### 2.7 三种输出格式

| 格式 | 用途 | 示例 |
|------|------|------|
| `raw`(默认) | AI 解析友好,最紧凑 | `2024-01-15T10:30:45Z {job=api} Request received` |
| `json` | 程序化处理 | 完整 JSON 结构 |
| `text` | 人类可读 | 编号 Stream + 时间戳日志行 |

#### 2.8 已知 Bug 及修复

**时间戳 2262 年 Bug**:Loki 返回纳秒时间戳,早期实现使用 `time.Unix(ts, 0)` 将纳秒当秒处理,导致显示 2262 年。

修复:`time.Unix(0, int64(ts))` — 第一个参数为 0 秒,第二个参数为纳秒。

### Phase 3:Docker 化与本地环境

#### 3.1 Dockerfile(多阶段构建)

```dockerfile
# Stage 1: 编译
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download          # 利用缓存层
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o loki-mcp-server ./cmd/server

# Stage 2: 运行(最小镜像)
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/loki-mcp-server .
EXPOSE 8080
ENTRYPOINT ["./loki-mcp-server"]
```

**关键点**:
- `CGO_ENABLED=0`:静态链接,不依赖 glibc
- 先 COPY go.mod/go.sum → `go mod download`:利用 Docker 层缓存加速构建
- 最终镜像基于 `alpine:latest`:最小化攻击面

#### 3.2 Docker Compose(5 服务完整环境)

```yaml
services:
  loki-mcp-server:    # MCP Server :8080
    depends_on:
      loki:
        condition: service_healthy  # 等 Loki 就绪

  loki:               # 日志存储 :3100
    healthcheck:      # /ready 端点检查
      test: ["CMD-SHELL", "wget -q --spider http://localhost:3100/ready || exit 1"]

  grafana:            # 可视化 :3000
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning  # 预配置数据源

  promtail:           # 日志采集
    volumes:
      - /var/log:/var/log                    # 采集宿主机日志
      - /var/run/docker.sock:/var/run/docker.sock  # 采集容器日志

  log-generator:      # 测试日志生成器
    command: |        # 每 5 秒生成 INFO/ERROR 日志
      while true; do echo "INFO: ..."; sleep 5; done
```

**服务依赖链**:`log-generator → promtail → loki → loki-mcp-server`,Grafana 独立。

### Phase 4:测试策略

#### 4.1 单元测试

重点覆盖时间戳解析(bug 高发区):

```go
func TestFormatLokiResults_NoYear2262Bug(t *testing.T) {
    testCases := []struct {
        name         string
        timestampNs  string
        expectedYear string
    }{
        {"Current", "1705312245000000000", "2024"},   // 2024-01-15
        {"Recent",  "1700000000000000000", "2023"},   // 2023-11-14
        {"Future",  "1800000000000000000", "2027"},   // 2027-01-11
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            // 构建 LokiResult → formatLokiResults → 断言年份
        })
    }
}
```

**测试用例覆盖**:
- 正常时间戳解析
- 多条日志时间戳
- 非法时间戳回退
- 空结果处理
- 当前时间回归
- 2262 年 Bug 回归(表驱动,3 个时间点)

#### 4.2 运行测试

```bash
make test                                    # 全部测试
go test -coverprofile=coverage.out ./...    # 带覆盖率
go tool cover -func=coverage.out            # 查看覆盖率
go test -race ./...                         # 竞态检测
```

#### 4.3 集成测试脚本
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐