portal-logging について
portal-loggingは、アプリのログをポータルの「ログとモニタリング」アプリに接続するスキルです。アプリのデプロイや障害対応を担うチーム向けに、認証情報、収集・送信方法、検証とトラブルシューティングを説明し、監査や課金データとの違いも示します。
主な機能
- 01コレクターとアプリ内送信の方式
- 02サービス認証情報の設定
- 03バッチ送信と応答の検証
- 04ログ項目とレベルの規約
- 05クエリとトラブルシューティング
活用シーン
- 01コンテナやファイルログの収集設定
- 02アプリから構造化ログを一括送信
- 03送信を検証してポータルでログ検索
- 04認証、権限、ストリーム名の問題を調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ xgent-ai/ skills --skill portal-loggingインストール後、Claude Code で/portal-loggingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/collectors.mdcontract.mdscripts/smoke-ingest.sh詳細
プロジェクトの 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. 三个前提(不成立时,每一步都会以「看起来成功」的方式失败)
- 平台把「日志与监控」登记为平台基础服务应用,且默认授予里含
observability.ingest。 这是平台管理员在门户控制台做的一次登记。没有它:你换令牌拿到 HTTP 200、scope字段却是空的,直到第一次写入才 403。——第一天接日志的团队九成栽在这里,先查它。 - 你的 App 在目标租户已安装且可用。 令牌里的
azp绑着你的 App key,签发时会检查「这个租户装没装你」。没装 ⇒APP_NOT_INSTALLED。语义是对的:你没在这个租户里跑,就不该有这个租户的日志。 - 你有自己的服务账号凭据。 平台部署你的服务时注入
<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. 冒烟验收(接完逐条走一遍,都过才算接上)
- 换出来的令牌
scope确实含observability.ingest(不是空)。 - 写一条带标记的记录,响应 HTTP 200 且
status[]里failed:0。 - 在「日志与监控」App 里按这个标记搜到了它,流名是
app_<你的 key>_<stream>。 - 故意打一个多行栈,界面里它是一条记录。
- 打一条 error 和一条 info,界面里级别分得开。
- 把采集器停掉再起来,不重放历史、也不丢新行。
别做的事
- 别为日志自建一套网关/存储/密钥体系——这条链路已经存在,重造的那份不会有人运维。
- 别把审计、用量、通知塞进日志流(§0)。
- 别在生产刷 debug 级别:写入是计量项(按字节),也会把有用的行淹掉。
- 别把别的 App 的日志代写进自己的流:
azp决定前缀,代写要么被拒,要么污染自己的命名空间。