Repository navigation
Remote
把当前插件明确选择的日志、异常现场(Incident)和手动 Incident 投递到 Remote Server,用于线上排障。
| 模块名 | 制品 | 自动带入 |
|---|---|---|
remote |
me.kzheart.klib:klib-remote |
core |
- Kotlin 实现,Java 与 Kotlin 直接使用同一客户端;产物保持 Java 8 字节码边界。
- 和 Guard 的授权、实例绑定、风控、制品签名及其身份数据完全独立:两者不调用彼此、不共享 Key,也不把 Remote 安装标识当作授权证明。
- 不采集心跳、
latest.log、根 Logger、其他插件 Logger 或平台级日志。 - 没有版本查询,也没有
Redactor或平台自动脱敏。事件正文是否包含敏感内容由插件开发者负责,详见 Remote 安全边界。
推荐通过 Gradle 插件在构建期把端点、公开 Key 和能力上限写入插件 JAR:
klib {
main("com.example.market.MarketPlugin")
modules {
remote()
}
remote {
endpoint("https://remote.example.test")
publicKey("rpk_test_0123456789abcdefghijklmnopqrstuvwxyzABCDEFG")
exceptions.set(true)
logs.set(true)
manualIncidents.set(true)
}
}依赖、直接引用制品和打包方式见 构建与打包。
- 示例端点和 Key 仅用于展示格式,实际接入时替换为 Remote 服务提供的值。
-
KlibRemoteAccess由 Gradle 插件生成在main(...)类所在的包中,其他包使用时需要导入。常量和 DSL 的完整规则见 Klib Gradle 插件仓库。
公开 Key: rpk_live_... 和 rpk_test_... 是公开项目 Key。
- 只能读取
/ingest/v1/settings,并向/ingest/v1/batches写入事件,随 JAR 分发不是泄密。 - Key 不是用户、服务器、正版插件或事件来源的真实性证明。
能力默认全关:
- 所有能力默认关闭,客户端启动时也 fail-closed。
- 只有
refreshPolicy()成功后,才按 构建能力 ∩ 远端 Key/商品策略 采集。 - 远端策略只能进一步收紧,永远不能开启构建时未允许的能力。
- 在初始化流程中建立客户端、异步交付器和当前插件专属 Logger,并安排释放。
- 使用
KPlugin时入口是无参setup()(需要 Scope 时用context().scope()),不能覆盖其 final 的onEnable/onDisable。 - 安装标识首次
get()和交付队列初始化会访问磁盘:放在受控的后台初始化流程中运行,完成后再把资源交给活动 Scope。 - 初始化期间如果插件已经关闭,应立即释放刚创建的 delivery 和 logger。
- 不要在主线程阻塞等待网络或磁盘操作。
下面是构造代码片段(KPlugin 实例上下文):
import java.nio.file.Path;
import me.kzheart.klib.remote.InstallationId;
import me.kzheart.klib.remote.RemoteCapabilities;
import me.kzheart.klib.remote.RemoteClient;
import me.kzheart.klib.remote.RemoteDelivery;
import me.kzheart.klib.remote.RemoteEnvironment;
import me.kzheart.klib.remote.RemoteLogger;
RemoteCapabilities capabilities = RemoteCapabilities.builder()
.exceptions(KlibRemoteAccess.EXCEPTIONS_ENABLED)
.logs(KlibRemoteAccess.LOGS_ENABLED)
.manualIncidents(KlibRemoteAccess.MANUAL_INCIDENTS_ENABLED)
.build();
RemoteClient client = RemoteClient.http(
KlibRemoteAccess.ENDPOINT,
KlibRemoteAccess.PUBLIC_KEY,
capabilities,
InstallationId.forProduct("example.market", getDataFolder().toPath()),
new RemoteEnvironment("2.4.0", "Paper 1.21.4", "17", "Linux"));
Path queue = getDataFolder().toPath().resolve("remote-queue");
RemoteDelivery delivery = RemoteDelivery.builder(client, queue).build();
RemoteLogger remote = RemoteLogger.builder("example.market", delivery)
.policy(client::policy)
.build();端点校验:
| 入口 | 允许的端点 |
|---|---|
RemoteClient.http |
只接受 HTTPS |
insecureLoopback |
只允许 http://127.0.0.1、localhost 或 ::1,用于本地测试 |
端点不能含 userinfo、query 或 fragment;校验失败也不会回显端点内容。
安装标识: InstallationId.forProduct(productId, dataDirectory) 创建惰性供应器,首次 get() 时在 dataDirectory/.klib-remote/ 读取或生成并持久化 16 字节安全随机值。
- 不读取 IP、MAC、主机名、硬件、端口或路径内容。
- 同一商品的数据目录跨重启稳定;不同商品不同;删除数据目录后会变。
- 它是匿名、商品作用域的安装标识,不是授权或反滥用身份。
环境摘要: 每个批次附带 RemoteEnvironment(pluginVersion, minecraft, java, os) 四个字段。不要把用户、密钥、完整配置或服务器路径放进去。
RemoteClient 公开 v1 入口:
| 方法 | 行为 |
|---|---|
refreshPolicy() |
请求 settings,解析 RemoteSettings,再与 RemoteCapabilities 取交集。失败抛 IOException,但保留上次已生效策略 |
policy() |
首次成功前为 paused 的 fail-closed 策略 |
settings() |
最后一次成功的 settings;尚未成功时为 null
|
sendBatch(...) |
同步网络 I/O,通常只用于受控工具;Minecraft 业务路径应使用 RemoteDelivery
|
- 通常无需在业务线程调用
refreshPolicy():RemoteDelivery会在后台立即刷新,之后默认每 60 秒刷新一次,可用settingsRefreshInterval(Duration)调整。 - 服务端
accepting_events=false时,客户端把有效策略置为 paused。日志最低等级和采样率仍保留在RemotePolicy中,但不发送事件。 - 交付器首次成功取得 settings 前绝不发送;后续刷新失败则保留最近一次成功设置并按退避重试。
展开:settings 的 v1 schema 校验与 Limits 预算 getter
- settings 以严格 v1 schema 解析:顶层、policy、limits、retention 的全部字段必须存在,且类型、层级与范围正确。
- 未知字段、重复字段、尾随 JSON 或把字段放错层级,都作为协议错误 fail-closed。
-
Limits公开 Key、IP、installation、ASN 与商品突发五个每分钟预算 getter,便于交付器和诊断界面解释服务端限制。
RemoteLogger 是插件自己的结构化入口:info、warn、error 以及 log(Level, message, context)。
- 始终尝试把日志放入内存日志窗口;是否独立投递由 logs 策略、最低等级和采样率决定。
-
bridge(Logger)只桥接所传入的同名 Logger,返回可释放的Disposable。 - 不会监听根 Logger、子 Logger、
latest.log或其他插件。
import java.util.logging.Level;
import me.kzheart.klib.remote.RemoteLogContext;
RemoteLogger remote = RemoteLogger.builder("example.market", delivery)
.policy(client::policy)
.build();
remote.log(Level.INFO, "listing refreshed",
RemoteLogContext.builder()
.context("listing_count", 42)
.mdc("request_id", "request-example")
.tag("listing")
.build());
try {
reloadListings();
} catch (Exception error) {
remote.captureIncident("listing-reload", error);
}reloadListings 是业务方法。
RemoteLogContext 限制:
| 项 | 上限 |
|---|---|
| context | 32 项 |
| MDC | 32 项 |
| 键 / 值 | 64 / 1024 UTF-8 字节 |
| 标签 | 16 个,各 64 字节 |
它不会清理内容。不要传入玩家聊天、命令、Token 或配置正文,除非你明确允许把它们上传。
| 方法 | 由哪个能力控制 | fingerprint |
|---|---|---|
captureIncident(name, throwable) |
exceptions |
异常类型、cause 链和规范化堆栈 |
captureManualIncident(name, attributes) |
manualIncidents |
manual:<name>(没有 Throwable) |
每一次捕获都是一个 Incident;服务端把同 fingerprint 的多次现场聚合为一个 Issue。
| 内容 | 默认上限 |
|---|---|
| 异常快照:每层 Throwable 的类型、message、完整到预算的 stack、cause 和 suppressed | 32 层、每层 256 帧、每层 32 个 suppressed,整棵 Throwable 图 256 个不同节点 |
breadcrumb(category, message, context) 记录的有界事实 |
64 条 / 64 KiB |
| 当前插件 Logger 近期的有界日志窗口(即使日志流关闭仍可进入 Incident) | 128 条 / 256 KiB |
DiagnosticContributor 结果 |
最多 16 个;一次 Incident 共用 100 ms 采集时间;每个结果最多 64 项 / 64 KiB |
当前 RemoteOperation(如存在)、其父操作和祖先 |
— |
- 共享的 cause 或 suppressed 节点按对象身份去重,防止循环和分支扩张绕过总预算。
- 每层 operation 带可选
duration_ms:SDK 用本地单调时钟计算结束时间减开始时间,尚未结束为null。 -
RemoteOperation.wrapCurrent(...)可包装Runnable、Callable、Supplier跨异步边界;RemoteScheduler为KScheduler自动包装。异步激活作用域不会提前结束被传播 operation 的计时。 - Contributor 的名称、采集和结果字符串化都在同一个有界 executor 与全局 deadline 内执行;超时、拒绝和异常编码为状态,不会阻断业务。
JSON 快照全有或全无: RemoteEvent payload、日志 context/MDC 和 Breadcrumb context 在入队前建立不可变 JSON 快照。
- 最大深度 32、总节点 4096、单个容器 256 项。
- 循环、非法类型或任一上限超限时,整条事件被拒绝,不会截断成看似有效但缺上下文的事件。
展开:通过 payload.confirmed_cause 显式提交确认原因
- 确定性校验器或解析器可以在自定义 Incident 的
payload.confirmed_cause中显式提交target_type、target_id、summary。 - 这是唯一会进入 control
confirmed_cause档的 marker。 - 标准
captureIncident不会把 Throwable cause、message 或 operation outcome 自动升级为确认原因。 - 需要该能力的自定义采集,用
RemoteEvent.of("incident", fields)构造完整 v1 Incident,并仍由现有 delivery 提交。
KlibDiagnosticContributor 将核心模块的 DiagnosticSource 接到 Contributor。当前 Config、Scheduler、
Command 与 Data 实现提供轻量内存快照;它们的语义分别见
Config、Core、Command 与 Data。
Contributor 不得在采集时读文件、访问网络或等待外部服务。
RemoteDelivery 的 submit/accept 只进入内存邮箱:
- 调用线程不做磁盘或网络 I/O,也不会把交付、序列化或队列故障抛回业务。
-
submit返回的CompletionStage<Boolean>:事件原子写入本地队列后为true;关闭、邮箱锁争用、容量淘汰、无效事件或本地写入失败为false,不会以异常完成。
默认值:
| 项 | 默认 |
|---|---|
| 磁盘预算 | 16 MiB |
| 磁盘事件数 | 4096 条 |
| TTL | 24 小时 |
| 内存邮箱 | 256 条 |
| 每批 | 最多 100 条,正文合计 1 MiB |
| 单事件 | 64 KiB,超限整体拒绝而不截断 |
- Incident 比普通日志优先;容量不足时先淘汰最旧日志以保留 Incident。
- 队列保存完整原始事件正文、安装标识和环境快照,属于敏感本地数据;目录必须由插件私有化保护,删除它会丢失未交付事件。
跟随服务端限制: 每次后台 settings 刷新后:
- 交付器把本地配置与服务端下发的
max_event_bytes、max_batch_events、max_decompressed_bytes取较小值。 - 组批后对**完整 batch(含安装与环境 envelope)**实际 gzip,确认未超过
max_compressed_bytes。 - 服务端收紧限制后,已在队列中但超限的单个事件整条移除并计入
droppedEvents(),不会截断,也不会永久热重试;过大的候选 batch 会缩小后再尝试。
队列目录: 同一个队列目录绑定端点和公开 Key 的 SHA-256 身份,换端点或 Key 必须使用新目录。
展开:队列目录的权限、身份与损坏处理
- 整条父目录链不允许 group/other 或非 owner 写入。
- 文件系统必须提供稳定目录身份和可验证的 owner-only POSIX 权限或 ACL,否则构造时 fail-closed。
- 目录被替换,或父目录权限在运行期间变得可写,都会停止后续读写删除。
- 队列另有单进程锁和原子写入。
- 损坏项移入受限的
quarantine,不会因为声明长度分配无界内存。
失败采用带抖动的指数退避,默认 1 秒至 60 秒。
| 情况 | 行为 |
|---|---|
429 |
尊重 Retry-After(可超过退避上限) |
403 |
视为暂停,至少停 5 分钟再试 |
401(包括 settings 刷新) |
终止交付器并保留本地队列,之后提交返回 false
|
完整收据中的 accepted、duplicate、rejected
|
都从队列移除 |
| 收据不完整、无效或网络失败 | 保留整个批次,用同一 event_id 重试 |
收据使用严格 JSON schema:
- 重复字段、未知字段、类型错误、尾随内容、索引、计数或
event_id不一致,都视为协议失败并保留队列。 -
accepted/duplicate不得携带error;rejected必须携带非空error。
| 调用方使用 | 客户端内部负责 |
|---|---|
RemoteEvent / Incident 工厂 |
字段检查与事件深快照 |
RemoteDelivery.submit / accept
|
有界内存邮箱、单工作线程、磁盘队列与重试时钟 |
RemoteOperation / RemoteLogger
|
上下文恢复、采集预算、Contributor 隔离与超时 |
RemoteClient |
请求编码、严格收据校验与策略状态 |
这些处理由 Kotlin 实现,Java 继续使用静态工厂、Builder 和普通回调。RpkTransport、DiagnosticContributor 保留 Java 平台类型、默认方法与异常声明;RemoteEvent 的薄 Java 外壳保留既有子类和静态工厂契约。
实现语言不会自动打开采集、增加敏感字段或扩大远端权限;是否构造并提交事件仍由业务显式决定。
- 把
RemoteLogger、桥接返回的Disposable和RemoteDelivery纳入插件生命周期,停服时调用dispose()。 -
RemoteLogger.dispose()会停止 Contributor 线程。 -
RemoteDelivery.dispose()不等待在途网络请求。 -
RemoteClient没有关闭方法。 - 安装标识首次
get()和队列初始化访问磁盘;sendBatch(...)是同步网络 I/O。都不要放在主线程阻塞等待。 -
submit/accept在调用线程只进内存邮箱,不做磁盘或网络 I/O。 - Contributor 不得在采集时读文件、访问网络或等待外部服务。
- 本模块没有旧诊断协议的兼容路由、兼容 shim 或导入器。
- 旧版未校验诊断数据不会导入,也不得在 Remote 中视为可信数据。
- 协议细节见 Remote 协议 v1。
- Remote 服务端实现和部署配置不属于本公共 JVM 客户端仓库。
展开:control 面能看到什么,以及 linked / confirmed_cause / nearby 的含义
服务端 control 面提供:
- Issue 四维环境分布、日志前后文,以及明确的 Incident/Issue 关联;
- Incident 三档因果关系;
- 安装筛选与最近 Issue、按日 rejected 趋势;
- 公开接入 origin、写入安全闸状态、limits / build 只读查询、本地会话恢复。
关联规则:
- 日志关联只接受 Incident 日志窗口中显式携带的
(key_id,event_id)。 - Incident 的
linked、confirmed_cause、nearby分别代表显式 operation 边、客户端显式原因声明和单纯时间接近。 - 不按时间或文本自动推断根因。
完整路由与保留语义由对应 Remote 服务端版本的私有运维文档定义。
- 公开 Key 不证明真实性:不要把 Remote 安装标识或 Key 当作授权、正版或反滥用依据。
- 远端策略只能收紧:构建时没打开的能力,服务端无法开启。
-
RemoteLogContext、Breadcrumb 和 payload 不做脱敏,敏感内容需要插件自己排除。 - 超限事件整条拒绝或移除,不会截断。
- 换端点或公开 Key 时,必须换新的队列目录。
- 快速开始:插件完整入口。
-
Config、Core、Command、Data:可接入的
DiagnosticSource。 - Remote 安全边界、Remote 协议 v1。
-
Klib Gradle 插件仓库:
KlibRemoteAccess常量与remote { }DSL。