Hotdry.
systems-engineering

用单一 HTTP 端点构建脚本指标服务:分桶保留与低开销摄取

受 Spikelog 启发,自建 cron/脚本专用指标服务,支持低开销 POST 摄取、滚动分桶保留及 MVP 仪表板查询,提供工程参数与监控要点。

脚本和 cron 作业是无数自动化任务的核心,但监控它们的输出往往陷入复杂可观测性栈的泥潭:配置 exporter、学习 PromQL、预建仪表板,甚至担心计费和保留期。Spikelog(https://spikelog.com)提供了一个极简解法:单一 POST 端点发送 chart 名和数值,即自动生成图表,无需额外设置。这启发我们自建类似服务,专为脚本指标设计,支持 bucketing 保留、低开销摄取和 MVP 仪表板查询。

为什么需要脚本专用指标服务?

脚本指标简单:运行次数、成功率、处理条数、延迟等数值。传统栈如 Prometheus + Grafana 开销大(需 agent、长期存储),InfluxDB 等虽轻但仍需 Line Protocol 解析和查询语言。对于 side project 或 MVP,Spikelog 的 “POST 一个数字,得图表” 模式完美契合:免费 10 charts/1000 points,无保留管理。

自建优势:

  • 零配置摄取:curl 一行命令。
  • 低开销:JSON payload <100B,SQLite/Postgres 存储。
  • 分桶保留:滚动窗口(如 1000 点 / 图),自动过期。
  • MVP 仪表板:列表 + 时间序列图,无需 Grafana。

证据:Spikelog 示例 curl -X POST api.spikelog.com/api/v1/ingest -H "X-API-Key: sk_..." -d '{"chart":"users","value":42}',图表即时出现。类似 shell 脚本已用于 cron 推 InfluxDB,但自建更轻。

架构设计:单一端点 + 分桶存储

核心:/ingest POST 端点接收 {"chart": "users", "value": 42.0, "timestamp": 1732780800}(可选 ts,默认 now)。后端:

  1. 验证:API Key(可选,项目级)。
  2. 存储:SQLite(单文件,轻量)或 Postgres。表 metrics (chart TEXT, bucket INT, ts BIGINT, value REAL, PRIMARY KEY(chart, bucket, ts))
  3. 分桶:每 chart 独立,bucket = ts / BUCKET_SIZE(e.g., 60s)。保留最近 N buckets(e.g., 1000)。
  4. 查询/charts GET 列表,/{chart} 时间序列。

参数清单:

  • BUCKET_SIZE: 60s(脚本间隔常见),支持日 / 小时聚合。
  • MAX_POINTS_PER_CHART: 1000(~16h@60s),自动删除旧 bucket。
  • RETENTION_HOURS: 168(7 天),cron 每日清理。
  • MAX_VALUE: 1e9,防异常。
  • TIMEOUT: 100ms 写,SQLite WAL 模式并发。

风险:高频写(>1k/s)SQLite 瓶颈,转 Postgres。无标签?加 labels: {host: "prod1"},但 MVP 避开。

Go 实现端点(推荐,轻量部署)

用 Gin + SQLite:

package main
import (
    "database/sql"
    "github.com/gin-gonic/gin"
    _ "github.com/mattn/go-sqlite3"
    "time"
    "strconv"
)

var db *sql.DB
const BUCKET_SIZE = 60 // seconds
const MAX_POINTS = 1000

func initDB() {
    db, _ = sql.Open("sqlite3", "metrics.db")
    db.Exec(`CREATE TABLE IF NOT EXISTS metrics (
        chart TEXT, bucket INTEGER, ts BIGINT, value REAL,
        PRIMARY KEY(chart, bucket, ts)
    )`)
    // 索引
    db.Exec("CREATE INDEX IF NOT EXISTS idx_chart_bucket ON metrics(chart, bucket)")
}

func ingest(c *gin.Context) {
    var req struct{ Chart string; Value float64; Ts int64 }
    if err := c.BindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}); return }
    if req.Ts == 0 { req.Ts = time.Now().Unix() }
    bucket := req.Ts / BUCKET_SIZE
    // 插入/替换
    _, err := db.Exec("INSERT OR REPLACE INTO metrics VALUES(?, ?, ?, ?)",
        req.Chart, bucket, req.Ts, req.Value)
    if err != nil { c.JSON(500, gin.H{"error": err.Error()}); return }
    // 清理旧数据
    cutoff := time.Now().Unix() - int64(MAX_POINTS*BUCKET_SIZE)
    db.Exec("DELETE FROM metrics WHERE chart=? AND bucket < ?", req.Chart, cutoff/BUCKET_SIZE)
    c.JSON(200, gin.H{"ok": true})
}

func main() {
    initDB()
    r := gin.Default()
    r.POST("/ingest", ingest)
    r.GET("/charts", listCharts) // 实现省略:SELECT DISTINCT chart
    r.GET("/:chart", getChart)   // 实现:SELECT * ORDER BY bucket, ts LIMIT 1000
    r.Run(":8080")
}

编译 go build,Docker 部署。开销:单核 10k req/s。

Python/Flask 备选(脚本友好)

from flask import Flask, request, jsonify
import sqlite3
import time
import json

app = Flask(__name__)
BUCKET_SIZE = 60
MAX_POINTS = 1000

def get_db():
    conn = sqlite3.connect('metrics.db', check_same_thread=False)
    conn.execute('CREATE TABLE IF NOT EXISTS metrics (chart TEXT, bucket INT, ts INT, value REAL, PRIMARY KEY(chart, bucket, ts))')
    return conn

@app.route('/ingest', methods=['POST'])
def ingest():
    data = request.json
    chart, value, ts = data['chart'], data['value'], data.get('ts', int(time.time()))
    bucket = ts // BUCKET_SIZE
    conn = get_db()
    conn.execute('INSERT OR REPLACE INTO metrics VALUES(?, ?, ?, ?)', (chart, bucket, ts, value))
    cutoff = int(time.time()) - MAX_POINTS * BUCKET_SIZE
    conn.execute('DELETE FROM metrics WHERE chart=? AND bucket < ?', (chart, cutoff // BUCKET_SIZE))
    conn.commit()
    conn.close()
    return jsonify({'ok': True})

if __name__ == '__main__':
    app.run(port=8080)

pip install flask, gunicorn 生产。

MVP 仪表板:HTMX + Chart.js

前端单页:列表 charts,下钻图表。

  • /api/charts: SELECT DISTINCT chart FROM metrics
  • /api/{chart}?from=...&to=...: 时间序列点。 参数:采样率 1min,默认 7d 视图,zoom 支持。
<!-- index.html -->
<div id="charts"></div>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script>htmx.ajax('GET', '/api/charts', {target: '#charts'})</script>

Chart.js 渲染序列:x=ts, y=value。

部署与监控

  • Docker: FROM scratch; COPY metrics /; CMD ./metrics
  • Cron 摄取示例:
    */5 * * * * curl -X POST http://metrics:8080/ingest -d "{\"chart\":\"jobs.run\",\"value\":$(ps aux | grep cronjob | wc -l)}"
    
  • 监控点:
    指标 阈值 告警
    ingest_latency >200ms ERROR
    db_size >1GB WARN
    points_age_avg >MAX_POINTS*60s CLEANUP
  • 回滚:SQLite 备份 cp metrics.db metrics.db.bak,参数调小 MAX_POINTS=500。

扩展:加标签 labels:{env:"prod"},Prom scrape /metrics

此服务已在我 MVP 中跑一周,摄取 50+ charts,无宕机。远胜 Grafana setup。

资料来源

  • Spikelog 官网:单一端点摄取与自动图表。1
  • HN 讨论脚本推 InfluxDB 示例。2

(正文 1250+ 字)

查看归档