PostHog Logs MCPlogs-sparkline-query工具实战:日志量 Sparkline 的低成本查询参数详解与源码解析
【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog
本篇围绕 PostHog 日志产品(Logs)的 MCP 提示词文档 logs-sparkline.md 展开,讲清logs-sparkline-query工具的用途、全部查询参数、可复制的 JSON 示例,以及它背后的 REST 端点、SparklineQueryRunner与 ClickHouse 聚合 SQL 的完整实现链路。读完你可以直接在 MCP/Agent 场景中按规范构造 sparkline 查询,并理解其时间桶划分、Top 10 折叠、count/bytes排名等底层行为,从而在“先概览、后细查”的日志排障流程中做出正确的工具选型。
Sparkline 的定位:比全量日志查询便宜得多的概览工具
logs-sparkline.md 开篇即定义了该工具的定位:
Get a time-bucketed sparkline of log volume, broken down by severity or service. Use this to understand log volume patterns before querying individual log entries — it is much cheaper than a full log query.
即在按时间桶(time-bucketed)粒度上查看日志量曲线,可按 severity 或 service 拆分。官方建议把它作为排障的第一步:先理解日志量的时间分布规律,再决定是否需要用全量日志查询工具下钻到具体日志条目,因为 sparkline 的成本远低于完整日志查询(full log query)。
在 MCP 工具清单 tools.yaml 中,该工具注册为logs-sparkline-query,对应的 OpenAPI 操作是logs_sparkline_create,启用状态、只读(readOnly: true)、幂等,需要logs:read权限,响应仅保留results字段:
logs-sparkline-query: operation: logs_sparkline_create enabled: true scopes: - logs:read annotations: readOnly: true destructive: false idempotent: true title: Query logs sparkline description_file: ./prompts/logs-sparkline.md response: include: - results值得注意的是,同文件中logs-services-create的描述推荐将 sparkline 与它搭配使用——“high volume × non-zero error_rate is the natural alert candidate”,而logs-patterns的响应里也显式排除了patterns.*.sparkline、sparkline_buckets等字段以节省 token。也就是说,sparkline 是 PostHog 日志 MCP 工具族中一个被刻意设计为低 token 成本的体积概览原语,与 count、count-ranges、facet-values、patterns 等工具共同构成“先概览、再下钻”的查询分层。
请求结构:所有参数必须放在query内
文档强调了一个关键约束:所有参数都要放在query对象里,顶层字段会被拒绝(top-level fields are rejected):
{ "query": { "serviceNames": ["api"], "dateRange": { "date_from": "-1h" } } }这个约束在后端有对应的强制校验。视图 api.py 中的sparkline动作会先取出request.data["query"],再调用self._require_dict_query(query_data)校验query必须是字典——把参数散落在顶层的请求会被直接拒绝。请求体由_LogsSparklineRequestSerializer承载(api.py),其唯一字段就是query(即_LogsSparklineBodySerializer)。
REST 端点本身为POST /api/projects/{team_id}/logs/sparkline,这一点可以从测试 test_sparkline_query_runner.py 直接确认:
response = self.client.post(f"/api/projects/{self.team.id}/logs/sparkline", data={"query": query_params})因此,无论是走 MCP 工具logs-sparkline-query,还是直接调用 REST API,请求体结构完全一致:外层包一个query,内层才是具体过滤与拆分参数。
参数详解:文档参数与源码完整参数对照
query.dateRange:时间范围,默认最近一小时
date_from:范围起点。接受 ISO 8601 时间戳或相对格式:-1h、-6h、-1d、-7d。date_to:范围终点,格式相同;省略或传 null 表示“当前时间”。- 缺省整个
dateRange时,后端使用最近一小时(-1h)。
视图代码印证了默认值逻辑(api.py):
date_range_data = query_data.get("dateRange") date_range = self.get_model(date_range_data, DateRange) if date_range_data else DateRange(date_from="-1h")序列化器的 help_text 也写明 “Date range for the sparkline. Defaults to last hour.”(api.py)。
query.serviceNames:按服务名过滤
字符串列表,例如["api-gateway"]。对应 ClickHouse 中的service_name过滤条件(由LogsQueryRunner.where()生成,见 sparkline_query_runner.py 的{where}占位符)。
query.severityLevels:按严重级别过滤
取值限于trace、debug、info、warn、error、fatal;省略则包含所有级别。后端枚举与此严格一致(api.py):
severityLevels = serializers.ListField( child=serializers.ChoiceField(choices=["trace", "debug", "info", "warn", "error", "fatal"]), required=False, default=[], help_text="Filter by log severity levels.", )query.searchTerm:日志正文全文检索
对日志 body 做全文检索的字符串。
query.filterGroup:属性过滤器
用于收窄结果的属性过滤组,格式与query-logs工具的 filters 相同(参见 query-logs.md)。测试中的典型空过滤组写法可供参考:
"filterGroup": {"type": "AND", "values": [{"type": "AND", "values": []}]}query.sparklineBreakdownBy:按 severity 或 service 拆分
"severity"(默认):按严重级别拆分;"service":按服务名拆分,用于观察哪个服务产生的日志最多。
query.sparklineRankBy:按 count 或 bytes 排名(文档未提但后端支持)
logs-sparkline.md 只列出了上述参数,但从源码看,后端还接受一个sparklineRankBy字段(api.py):
sparklineRankBy = serializers.ChoiceField( choices=["count", "bytes"], required=False, help_text='Rank breakdown values by "count" (default) or "bytes" before collapsing the tail into "other".', )它决定“哪些拆分值保留独立序列、哪些被折叠进 other”的排名依据:按event_count还是bytes_uncompressed排名(见后文 SQL 解析)。此外,personId/sessionId也出现在 sparkline 请求序列化器中(api.py),属于按人/按会话限定查询范围的能力。
三个官方示例:错误量、按服务、按级别
文档提供了三个可直接复制的示例,覆盖最常见的三类排查意图:
最近一天的错误量(error volume over the last day)
{ "query": { "serviceNames": ["api-gateway"], "severityLevels": ["error", "fatal"], "dateRange": { "date_from": "-1d" } } }按服务查看日志量(log volume by service)
{ "query": { "serviceNames": ["api-gateway"], "sparklineBreakdownBy": "service", "dateRange": { "date_from": "-6h" } } }按严重级别查看日志量(log volume by severity)
{ "query": { "serviceNames": ["api-gateway"], "sparklineBreakdownBy": "severity", "dateRange": { "date_from": "-1d" } } }三个示例的共同点:都用date_from相对时间控制窗口,date_to缺省到“现在”;拆分维度通过sparklineBreakdownBy显式声明,不声明时默认按 severity。
响应结构:每个时间桶一条记录
响应是一个桶数组(MCP 层保留results)。从 OpenAPI 响应序列化器 api.py 可以看到每个桶的完整字段:
| 字段 | 类型 | 说明 |
|---|---|---|
time | string | 桶起始时间(ISO 8601) |
severity | string | 仅当sparklineBreakdownBy="severity"时出现 |
service | string | 仅当sparklineBreakdownBy="service"时出现 |
count | int | 该桶内的日志条数 |
bytes_uncompressed | int | 该桶内未压缩字节数之和 |
即拆分维度字段是“二选一”出现:severity 拆分的桶带severity,service 拆分的桶带service,与_calculate中动态生成的result_key一致(见下节)。
源码链路:从 REST 视图到 ClickHouse SQL
视图层:参数装配与运行模式
api.py 中sparkline动作的完整流程:
tag_queries(product=Product.LOGS, feature=Feature.QUERY)打点;- 校验
query必须是字典; - 解析
dateRange,缺省回退DateRange(date_from="-1h"); - 构造
LogsQuery,字段包括severityLevels、serviceNames、searchTerm、filterGroup、resourceFingerprint、personId、sessionId、sparklineBreakdownBy、sparklineRankBy; - 交给
SparklineQueryRunner,以ExecutionMode.CALCULATE_BLOCKING_ALWAYS同步执行; - 上报
logs sparkline queried用户行为事件(含是否有 searchTerm、过滤组、级别/服务数量、拆分维度等属性); - 返回
response.results。
运行器层:30 秒超时与默认值
sparkline_query_runner.py 中的SparklineQueryRunner继承LogsQueryRunner,关键设计有四处:
(1)收紧执行超时至 30 秒。常量SPARKLINE_PREVIEW_MAX_EXECUTION_SECONDS = 30的注释解释了动机:volume preview 必须“快速返回或快速失败”;bytes 拆分需要求和_bytes_uncompressed,而分钟级聚合投影(minute-aggregate projection)并不覆盖它,高流量服务可能回退到全表扫描,因此把执行上限压到 60 秒默认值以下,让慢预览立刻暴露错误而不是像挂起一样等待。注意settings属性是复制父类 settings 后仅更新max_execution_time,注释特别说明这是有意为之——新建一个HogQLGlobalSettings会悄悄重新打开父类刻意关闭的allow_experimental_object_type/allow_experimental_join_condition/transform_null_in等“bug 绕过”标志:
@cached_property def settings(self) -> HogQLGlobalSettings: return super().settings.model_copy(update={"max_execution_time": SPARKLINE_PREVIEW_MAX_EXECUTION_SECONDS})(2)拆分维度到 ClickHouse 字段的映射:
BREAKDOWN_DB_FIELD: dict[LogsSparklineBreakdownBy, str] = { LogsSparklineBreakdownBy.SEVERITY: "severity_text", LogsSparklineBreakdownBy.SERVICE: "service_name", } DEFAULT_BREAKDOWN = LogsSparklineBreakdownBy.SEVERITY(3)排名依据的映射(即sparklineRankBy的落地):
RANK_BY_FIELD: dict[LogsSparklineRankBy, str] = { LogsSparklineRankBy.COUNT: "event_count", LogsSparklineRankBy.BYTES: "bytes_uncompressed", } DEFAULT_RANK_BY = LogsSparklineRankBy.COUNT注释点出“rank by bytes matters when the caller charts bytes: the top talkers by volume are not necessarily the top talkers by size”——按条数最多的服务和按字节最多的服务不一定是同一批。
(4)Top 10 折叠:SPARKLINE_TOP_BREAKDOWN_VALUES = 10,超过 10 个的拆分值会被折叠成单一 “other” 行;注释说明这与 sparkline UI 实际绘制的内容一致,折叠不会丢失会被画出来的东西。
SQL 层:时间桶骨架 + 排名折叠的聚合查询
to_query()生成的 HogQL(sparkline_query_runner.py)结构值得逐层理解:
- 外层
am子查询是“时间脊柱”:用numbers(...)从date_from对齐到 interval 起点开始,按 interval 步进生成一串time_bucket,保证即使某个时间桶没有日志,响应里也有对应行(count为 0 而不是缺行)。 LEFT JOIN聚合子查询ac:- 最内层对
logs表按toStartOfInterval({time_field}, {one_interval_period})与拆分字段GROUP BY,聚合出count() AS event_count与sum(_bytes_uncompressed) AS bytes_uncompressed;WHERE {where} AND time >= ... AND time <= ...承载 serviceNames、severityLevels、searchTerm、filterGroup 等全部过滤条件; - 中间层用窗口函数
sum({rank_field}) OVER (PARTITION BY breakdown_value)计算每个拆分值在整个窗口内的总量,再dense_rank() OVER (ORDER BY breakdown_total DESC, breakdown_value ASC)排名; - 最外层
if(breakdown_rank <= {top_n}, breakdown_value, {other_label})把 Top 10 之外的值统一改写成 other 标签(BREAKDOWN_OTHER_STRING_LABEL)后再GROUP BY time, breakdown_value求和——折叠的是序列,不是丢弃数据。
- 最内层对
- 时间字段的投影优化:占位符
time_field在 interval 不是秒级时使用toStartOfMinute(timestamp)而非timestamp本身。注释解释了原因:sparkline 投影是聚合在toStartOfMinute(timestamp)之上的,若直接用timestamp即使外层再套toStartOfInterval也命中不了该投影。 - 行数上界:注释说明行数“由构造决定”有界——rollup 后每个时间桶最多 11 个拆分值(10 + other),bucket 目标把桶数控制在约 50 个附近,所以任意时间范围下界在约 550 行,
LIMIT 1000只是兜底。 - 结果按
time asc, breakdown_value asc排序。
结果整形:UTC 时间戳与动态维度键
_calculate()把 SQL 结果整形为 API 行(sparkline_query_runner.py):
- 拆分值缺失(
None或空串)时归一为"(no value)"; - 桶时间显式打上 UTC 时区,注释说明是为了“与日志行时间戳和前端比对用的 live_logs_checkpoint 序列化格式保持一致”;
- 维度键名由
sparklineBreakdownBy决定(severity或service),这正好解释了响应中severity/service二选一出现的机制。
测试佐证:折叠不丢量、bytes 排名选出不同的 Top 10
后端测试 test_sparkline_query_runner.py 用 25 个服务 × 48 个 30 分钟桶的构造数据验证了上述行为:
- 尾部折叠:
test_service_breakdown_collapses_the_tail_into_one_other_bucket断言拆分值集合恰好是SPARKLINE_TOP_BREAKDOWN_VALUES + 1(10 个自身 + 1 个 other),且折叠不丢量——所有行的 count 总和等于25 × 48,other 行的 count 恰好等于被折叠的 15 个服务的总和;同时断言总行数 < 1000(注释指出折叠前会是 49 桶 × 25 服务 = 1225 行,超出 1000 行上限)。 - count 与 bytes 排名差异:
test_rank_by_bytes_keeps_a_different_top_ten_than_rank_by_count中每个服务日志条数相同,但service-024每条日志的字节数是其余服务的 100 万倍;断言rank_by="count"时该服务不在 Top 10(会被埋进 other),而rank_by="bytes"时它必须是独立序列,且两种排名下bytes_uncompressed总和不变。 - 基础正确性:
test_sparkline_single_log验证单条日志窗口返回 1 行、count 为 1;test_sparkline_near_full验证完整窗口返回 49 个桶、count 总和为 900。
这些测试与 MCP 工具的定位互为印证:sparkline 被设计成“行数恒定有界、总量可核对”的概览查询,无论时间范围多大,响应规模都稳定在数百行以内。
实战建议:把它放进日志排障的第一步
结合 tools.yaml 中工具族的分工,一个典型的 Agent 排障路径是:
- 用
logs-services-create(Top 25 服务,各带 log_count / error_count / error_rate 与 per-service sparkline)确定值得关注的服务——它是官方推荐的“triaging which services are worth alerting on”的入口; - 对目标服务用本文的
logs-sparkline-query拉取按 severity(默认)或按 service 拆分的体积曲线,确认异常窗口(例如“最近一天 error/fatal 量”示例); - 拿到异常时间窗后再用
query-logs下钻具体日志条目,或用logs-count-ranges/logs-facet-values做进一步切分; - 若怀疑是周期性异常,可结合
logs-anomalies-scan(基于最多 6 周历史学习基线)验证。
参数选择上记住三点即可复现文档全部行为:参数一律包在query内;不传dateRange时默认-1h;不传sparklineBreakdownBy时默认按 severity 拆分、按 count 排名、Top 10 之外的值折叠进 other。这套约定与 REST 端点、序列化器默认值和 ClickHouse 聚合 SQL 的注释完全一致,可以直接照此在 MCP 或 API 客户端中构造请求。
【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考