mcpField
P
portal-logging

portal-logging

github.com/xgent-aiCloud & DevOps1110 installsUpdated today
Repository

About portal-logging

portal-logging is a skill for connecting application logs to the portal’s Logs and Monitoring app, aimed at teams deploying and troubleshooting apps. It covers credentials, collection and ingestion patterns, validation, and diagnosis, while distinguishing logs from audit and billing data.

Key features

  1. 01Collector and in-app ingestion patterns
  2. 02Service identity credential setup
  3. 03Batch ingestion and response checks
  4. 04Log field and level conventions
  5. 05Query and troubleshooting guidance

Use cases

  1. 01Configure collection for container or file logs
  2. 02Send structured logs in batches from an app
  3. 03Verify ingestion and find logs in the portal
  4. 04Diagnose identity, permission, or stream-name issues

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/xgent-ai/skills --skill portal-logging

After installing, type/portal-loggingin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
collectors.md
contract.md
scripts/
smoke-ingest.sh

Details

From the project README

portal-logging · 把 App 的日志接进「日志与监控」

一条链路四段,两种身份:

你的进程  →  采集器  →  POST /svc/observability/v1/ingest/<stream>  →  在「日志与监控」App 里看
(产生)     (搬运)    ↑ 服务身份(机器的钥匙)                        ↑ 用户身份(人的票)

两种身份不能互换,这是整条链路最常见的困惑来源:采集器只能写、不能查;人只能查、不能写。拿服务密钥去查会得到 401,那不是配置错。

路径约定:本 skill 里的 apps/… deploy/… docs/… 都在门户仓。在你自己的 App repo 里它们不存在——它们标注的是「门户侧的事实在哪」,不是让你去打开的文件。你需要的一切在本 skill 的 references/ 与 scripts/(自包含,路径相对于本 SKILL.md 所在目录)。运行脚本前,在 App repo 根目录按实际加载位置设置 SKILL_DIR="<本 SKILL.md 所在目录>"。

按任务读

任务读
配采集器(容器 stdout / 文件 tail / 应用内直打),含多行合并与级别判定references/collectors.md
端点、凭据、请求/响应形状、查询 API、错误码矩阵、计量references/contract.md
只想先打通一条,验证链路是活的"$SKILL_DIR/scripts/smoke-ingest.sh"(改环境变量直接跑)

0. 先判断:你要接的真的是「日志」吗

日志流是给人排障看的时间序列文本。下面这些各有专用面,塞进日志流会同时失去两边的能力:

  • 审计(谁在什么时候改了什么,要可追溯、不可篡改、要留存)→ 门户审计面(audit.write),不是日志。
  • 用量/计费(要精确、要对账)→ 平台计量面。日志会被采样、会过期。
  • 给用户看的消息(提醒、待办、通知)→ 门户通知域。
  • 指标与链路(数值时间序列 / trace)→ 同一个服务的 OTLP 入口,形状与日志不同,见 contract.md。

判据:这条记录丢了会不会有人要赔钱或说不清责任。会 → 不是日志。

1. 三个前提(不成立时,每一步都会以「看起来成功」的方式失败)

  1. 平台把「日志与监控」登记为平台基础服务应用,且默认授予里含 observability.ingest。 这是平台管理员在门户控制台做的一次登记。没有它:你换令牌拿到 HTTP 200、scope 字段却是空的,直到第一次写入才 403。——第一天接日志的团队九成栽在这里,先查它。
  2. 你的 App 在目标租户已安装且可用。 令牌里的 azp 绑着你的 App key,签发时会检查「这个租户装没装你」。没装 ⇒ APP_NOT_INSTALLED。语义是对的:你没在这个租户里跑,就不该有这个租户的日志。
  3. 你有自己的服务账号凭据。 平台部署你的服务时注入 <PREFIX>_SA_CLIENT_ID / <PREFIX>_SA_CLIENT_SECRET(<PREFIX> = 你的 appKey 大写、- 换 _),另有 API_BASE_URL(门户内部源,换令牌用)与 PORTAL_BASE_URL(门户公开源,/svc/... 用)。 与门户同仓的后端没有「平台注入」这一步,同名的键在部署环境的 env 文件里——键名与用法完全一样,下文不再分叉。

不要为日志新建一套密钥,也不要把 observability.ingest 写进自己 manifest 的 serviceScopes:那是别人命名空间的 scope,写进去反而要求你把对方列进依赖;这条 scope 由平台在签发那一刻并进来,manifest 一个字都不用改。

2. 选写入身份

短期服务态令牌长期访问密钥(xsak_ 开头)
怎么拿POST {API_BASE_URL}/api/tokens/service,HTTP Basic = client_id:secret,body grant_type=client_credentials + tenant_id + scope门户控制台 › 服务账号 › 访问密钥(平台管理员签;租户自助建的服务账号同理)
有效期分钟级,要缓存 + 到期前续可无期限
谁用你的应用代码自己打点采集器进程(fluent-bit 这类不会自己换令牌的)
代价每次要一次 HTTP明文长期存在盘上,泄漏面大 ⇒ 只给 observability.ingest 这一条 scope,别多给

拿短期令牌时把它缓存到 expires_in - 10s,别每条日志换一次令牌——那是把日志量乘进了门户的负载。

3. 写入

POST {PORTAL_BASE_URL}/svc/observability/v1/ingest/<stream>
Authorization: Bearer <服务令牌 或 xsak_…>
Content-Type: application/json

[ {…记录1…}, {…记录2…} ]      ← 一次一批,别一条一个请求

成功判据要看两层:HTTP 200 且 响应体 code:200 且 status[] 里 failed:0。只看 HTTP 状态码会把「收下了但没落库」当成功——这正是「写入一直 200、界面永远查不到」的来源。

⚠️ 采集器代发(形态 A/B)时你根本看不到这个响应:采集器只在自己的日志里留一行 HTTP status=200,failed>0 那种「收下了没落库」它不会告诉你,401/403 也只是按重试上限丢掉——采集器不报错 ≠ 接通了。所以这两种形态的验收落在 §8 冒烟那一条 curl(那里你拿得到响应体)加上界面真的搜到;排障时再临时打开采集器的响应打印(见 collectors.md)。

流名:你写 <stream>,实际落在 app_<你的 appKey>_<stream>——前缀由令牌里的 azp 决定,你写不进别人的命名空间,也别在 <stream> 里再重复一遍自己的 key(会得到 app_foo_foo_console)。一个 App 按用途分流即可:console(进程标准输出)、access、job。

4. 字段口径(跨 App 一致,界面和排障才有共同语言)

字段必要性说明
message必备正文,一条一行
level强烈建议info / warn / error;产生方给的最准,采集器只能猜
service强烈建议进程/子服务名,一个 App 有多个进程时靠它分
host建议实例/主机名,多副本时靠它分
fd采集器填stdout / stderr

时间:落库的时间轴是到达时间,日志自带的时间只是一个普通字段。要让时间轴等于事件时间,就在记录里带上 _timestamp(微秒 epoch),并先用一条冒烟记录验证这个版本认不认——别假设。

级别:如果你的日志是文本行、由采集器搬运,采集器只能靠 stderr 和关键词猜,而 console.warn 一类在多数运行时也走 stderr ⇒ 例行 warn 会整批被判成 error。解法是产生方把级别写进正文(例如正文尾部的 JSON 里 "lvl":"error"),采集器优先取它。判定次序与可直接抄的实现见 collectors.md。

永远不要写进日志:令牌 / 密钥 / 密码、完整 query string(授权码与令牌常在里面)、用户个人信息与租户业务正文、大 blob。排障要的是能定位的 ID,不是内容本身——日志会被转发、留存、被平台管理员看到。

5. 选采集形态

形态适合代价
A 容器 stdout → 日志驱动 → 采集器容器化交付、应用零改动多一个采集器进程
B 采集器 tail 日志文件进程管理器把 stdout 写成文件的部署要处理轮转
C 应用内直接批量 POST想要结构化字段与精确级别、日志量小自己管批量/重试/背压

共性的四条(三种形态都适用,任一漏掉都会在生产上现形):批量发、异步发(不许在请求热路径上同步等远端)、失败丢弃而不是无限重试(日志不值得拖垮主流程)、多行合并(一个栈是一条记录,不是四十条)。

⚠️ 多行合并按 tag/流分缓冲。多个日志源共用一个 tag 时,A 源的栈会把 B 源随后那行吸进同一条记录——每个源(每个文件 / 每个容器 × stdout|stderr)一个 tag。

配置照抄 references/collectors.md。

6. 查

正常路径是人打开「日志与监控」App 查(需要 observability.read,由租户管理员同意授权)。程序化查询要用户身份的令牌,服务密钥打查询面是 401——先确认你是不是拿错了钥匙,再怀疑数据没进去。

7. 排错矩阵

症状多半是动作
换令牌 200,但响应里 scope 是空的平台没登记基础服务应用 / 默认授予里没有 ingest找平台管理员登记,§1.1
写入 403身份类型不对(拿用户票写)或 scope 里没有 ingest用服务身份;查签出来的 scope
写入 401凭证缺失 / 伪造 / 已撤销 / 已过期重签;轮换过的旧密钥立刻失效
换令牌报 APP_NOT_INSTALLED你的 App 没装在这个租户这是设计,不是 bug;换租户或先装
写入 200,但查不到① 只看了 HTTP 没看 status[];② 流名前缀猜错(是 app_<key>_<stream>);③ 时间范围——时间轴是到达时间按序查这三条
查询报 stream not found这个流一条都没写进去过回到上一行
查询 401、但写入 200拿服务密钥查换用户身份
只看到零星几条时间范围太窄;或你的进程本来就几乎不打日志先确认源头有没有行
一次报错被拆成几十条采集器没配多行合并collectors.md
一条记录里混了两个进程的行多个源共用一个 tag每源一个 tag
界面里只有别的 App 的流你的令牌 azp 不是你以为的那个自省一下令牌

8. 冒烟验收(接完逐条走一遍,都过才算接上)

  1. 换出来的令牌 scope 确实含 observability.ingest(不是空)。
  2. 写一条带标记的记录,响应 HTTP 200 且 status[] 里 failed:0。
  3. 在「日志与监控」App 里按这个标记搜到了它,流名是 app_<你的 key>_<stream>。
  4. 故意打一个多行栈,界面里它是一条记录。
  5. 打一条 error 和一条 info,界面里级别分得开。
  6. 把采集器停掉再起来,不重放历史、也不丢新行。

别做的事

  • 别为日志自建一套网关/存储/密钥体系——这条链路已经存在,重造的那份不会有人运维。
  • 别把审计、用量、通知塞进日志流(§0)。
  • 别在生产刷 debug 级别:写入是计量项(按字节),也会把有用的行淹掉。
  • 别把别的 App 的日志代写进自己的流:azp 决定前缀,代写要么被拒,要么污染自己的命名空间。

Comments

Markdown supported