@omni-api/plugin-observability 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # @omni/plugin-observability
2
+
3
+ OmniAPI 一站式可观测:**结构化日志 + OTel 追踪 + Prometheus 指标**,三件套全部 **peer-optional** —— 装哪个用哪个,都不装也能跑。
4
+
5
+ ## 安装
6
+
7
+ 最小:
8
+ ```bash
9
+ pnpm add @omni/plugin-observability
10
+ ```
11
+
12
+ 按需启用(业务自己决定要哪个):
13
+ ```bash
14
+ pnpm add pino # 结构化日志
15
+ pnpm add @opentelemetry/api # 分布式追踪(你需要自己配 SDK / Exporter)
16
+ pnpm add prom-client # Prometheus 指标
17
+ ```
18
+
19
+ ## 快速使用
20
+
21
+ ```ts
22
+ import { createObservability } from '@omni/plugin-observability';
23
+
24
+ const obs = await createObservability({
25
+ logger: { level: 'info', bindings: { service: 'orders', env: 'prod' } },
26
+ metrics: { prefix: 'myapp' },
27
+ });
28
+
29
+ defineProcedure({
30
+ name: 'order.create',
31
+ middleware: [obs.middleware /* 自动 log + trace + metrics */],
32
+ // ...
33
+ });
34
+
35
+ // HTTP 暴露 /metrics
36
+ import { createHttpAdapter } from '@omni/http';
37
+ const http = createHttpAdapter({ registry, logger: obs.logger });
38
+ http.server.get('/_metrics', async (_req, reply) => {
39
+ reply.header('content-type', 'text/plain; version=0.0.4');
40
+ return obs.metrics.render();
41
+ });
42
+ ```
43
+
44
+ ## 自动产出
45
+
46
+ 每个 procedure 调用都会产生:
47
+
48
+ ### 日志
49
+ ```
50
+ INFO procedure.success {"procedure":"order.create","source":"http","requestId":"req_xxx","durationMs":42}
51
+ WARN procedure.failure {"procedure":"order.delete","code":"FORBIDDEN","durationMs":3,"errorMessage":"..."}
52
+ ERROR procedure.failure {"procedure":"x","code":"INTERNAL",...} # 5xx 用 error 级别
53
+ ```
54
+ 日志级别策略:
55
+ - 4xx 业务错误 → `warn`(业务可预期)
56
+ - 5xx / 非 OmniError → `error`
57
+
58
+ ### 指标(Prometheus)
59
+ - `omni_procedure_total{name, source, status, code}` —— 计数
60
+ - status: `success` / `client_error` / `server_error`
61
+ - code: `OK` / `VALIDATION_ERROR` / `FORBIDDEN` / ...
62
+ - `omni_procedure_duration_seconds{name, source, status}` —— 直方图(默认桶 1ms~10s)
63
+
64
+ ### Trace
65
+ - 每个 procedure 一个 span:`procedure <name>`
66
+ - 自动属性:`omni.procedure.name`、`omni.source`、`omni.request_id`、`omni.user_id`、`omni.duration_ms`
67
+ - 异常自动 `recordException` + `setStatus(ERROR)`
68
+
69
+ ## 子模块单独使用
70
+
71
+ ```ts
72
+ import { createLogger, createTracer, createMetrics } from '@omni/plugin-observability';
73
+
74
+ const logger = await createLogger({ level: 'info' });
75
+ const tracer = await createTracer({ name: 'orders' });
76
+ const metrics = await createMetrics({ prefix: 'app' });
77
+ ```
78
+
79
+ ## 为什么 peer-optional?
80
+
81
+ 避免框架强绑定具体观测栈。业务可能:
82
+ - 只要日志(pino),不要 metrics
83
+ - 内部已经有自家 OTel 配置
84
+ - 只想跑测试,三个都不要
85
+
86
+ → 这些场景全部支持,零 if-else。
@@ -0,0 +1,204 @@
1
+ import { Logger, Middleware } from '@omni-api/core';
2
+
3
+ /** pino 的最小接口(避免引入完整类型) */
4
+ interface PinoLike {
5
+ trace(obj: unknown, msg?: string): void;
6
+ debug(obj: unknown, msg?: string): void;
7
+ info(obj: unknown, msg?: string): void;
8
+ warn(obj: unknown, msg?: string): void;
9
+ error(obj: unknown, msg?: string): void;
10
+ child(bindings: Record<string, unknown>): PinoLike;
11
+ }
12
+ interface CreateLoggerOptions {
13
+ /** 日志级别 */
14
+ level?: 'trace' | 'debug' | 'info' | 'warn' | 'error';
15
+ /** 全局绑定字段(如 service / version / env) */
16
+ bindings?: Record<string, unknown>;
17
+ /**
18
+ * 直接传入 pino 实例(已存在的 logger 复用)。
19
+ * 不传则尝试动态加载 pino;pino 不可用则 fallback 到 consoleLogger。
20
+ */
21
+ pino?: PinoLike;
22
+ /**
23
+ * 字段级脱敏路径(pino 原生支持的 path 语法)。
24
+ *
25
+ * 默认包含一组安全基线:authorization / cookie / token / password 等。
26
+ * 设 `redactPaths: false` 关闭脱敏;传数组则在默认基线上叠加。
27
+ */
28
+ redactPaths?: string[] | false;
29
+ /** 自定义替换值,默认 '[REDACTED]' */
30
+ redactCensor?: string;
31
+ }
32
+ /**
33
+ * 创建结构化 Logger。
34
+ *
35
+ * - 优先级:options.pino > 动态加载 pino > consoleLogger fallback
36
+ * - 异步加载 pino(peer-optional);为了让构造同步可用,提供了同步 fallback
37
+ *
38
+ * 业务通常这样用:
39
+ * const logger = await createLogger({ level: 'info', bindings: { service: 'orders' } });
40
+ */
41
+ declare function createLogger(options?: CreateLoggerOptions): Promise<Logger>;
42
+
43
+ /**
44
+ * Span 接口(OTel API 的最小子集)—— 用最小约束避免强依赖整个 OTel 类型库。
45
+ */
46
+ interface Span {
47
+ setAttribute(key: string, value: string | number | boolean): void;
48
+ setStatus(status: {
49
+ code: number;
50
+ message?: string;
51
+ }): void;
52
+ recordException(err: unknown): void;
53
+ end(): void;
54
+ }
55
+ /**
56
+ * Tracer 接口。
57
+ */
58
+ interface Tracer {
59
+ /** 创建 span 并执行 fn,正常 / 异常都会自动 end */
60
+ startActiveSpan<T>(name: string, attributes: Record<string, unknown>, fn: (span: Span) => Promise<T>): Promise<T>;
61
+ }
62
+ /** OTel SpanStatusCode 常量(避免引入 OTel 包) */
63
+ declare const SPAN_STATUS_OK = 1;
64
+ declare const SPAN_STATUS_ERROR = 2;
65
+ /** No-op Tracer:OTel 未安装时的兜底,保证业务代码不需要写 if 判断 */
66
+ declare const noopTracer: Tracer;
67
+ interface CreateTracerOptions {
68
+ /** Tracer 名称,默认 'omni-api' */
69
+ name?: string;
70
+ /** 直接注入 OTel TracerProvider 中拿到的 tracer(高级用法) */
71
+ tracer?: {
72
+ startActiveSpan: (name: string, fn: (s: Record<string, unknown>) => unknown) => unknown;
73
+ };
74
+ }
75
+ /**
76
+ * 创建 Tracer。
77
+ *
78
+ * - 优先级:options.tracer > 动态加载 @opentelemetry/api > noopTracer
79
+ * - OTel 未安装时返回 no-op,业务代码无需 if-else
80
+ */
81
+ declare function createTracer(options?: CreateTracerOptions): Promise<Tracer>;
82
+
83
+ /**
84
+ * 一个最小的 Counter 接口(避开 prom-client 完整类型)。
85
+ */
86
+ interface Counter {
87
+ inc(labels?: Record<string, string | number>, value?: number): void;
88
+ }
89
+ /**
90
+ * 一个最小的 Histogram 接口。
91
+ */
92
+ interface Histogram {
93
+ observe(labels: Record<string, string | number>, value: number): void;
94
+ }
95
+ /**
96
+ * Metrics 接口:
97
+ * - procedureTotal: 累计调用数(按 status / source / name 维度)
98
+ * - procedureDuration: 调用耗时分布(histogram,秒为单位)
99
+ * - render(): 输出 Prometheus 文本(接 /_metrics 用)
100
+ */
101
+ interface Metrics {
102
+ procedureTotal: Counter;
103
+ procedureDuration: Histogram;
104
+ /** 渲染 Prometheus exposition 文本 */
105
+ render(): Promise<string>;
106
+ }
107
+ interface PromClientLike {
108
+ Counter: new (config: {
109
+ name: string;
110
+ help: string;
111
+ labelNames: string[];
112
+ registers?: unknown[];
113
+ }) => Counter;
114
+ Histogram: new (config: {
115
+ name: string;
116
+ help: string;
117
+ labelNames: string[];
118
+ buckets?: number[];
119
+ registers?: unknown[];
120
+ }) => Histogram;
121
+ Registry: new () => {
122
+ metrics(): Promise<string>;
123
+ clear(): void;
124
+ };
125
+ register: {
126
+ metrics(): Promise<string>;
127
+ };
128
+ }
129
+ declare const noopMetrics: Metrics;
130
+ interface CreateMetricsOptions {
131
+ /** 指标名前缀,默认 'omni' */
132
+ prefix?: string;
133
+ /** Histogram 桶(秒),默认覆盖 1ms ~ 10s */
134
+ buckets?: number[];
135
+ /** 直接注入 prom-client(测试用) */
136
+ promClient?: PromClientLike;
137
+ }
138
+ /**
139
+ * 创建 metrics 实例。
140
+ *
141
+ * - prom-client 未安装则返回 no-op
142
+ * - 装了的话,自动注册两个指标到 **新建的独立 Registry**(避免和业务原有 register 冲突)
143
+ * render() 输出该 Registry 的指标
144
+ */
145
+ declare function createMetrics(options?: CreateMetricsOptions): Promise<Metrics>;
146
+
147
+ interface ObservabilityDeps {
148
+ logger: Logger;
149
+ tracer: Tracer;
150
+ metrics: Metrics;
151
+ }
152
+ interface ObservabilityOptions {
153
+ /** 是否在 info 级别打印每次成功调用(默认 true) */
154
+ logSuccess?: boolean;
155
+ /** 把 input 写到 log(默认 false,避免日志泄漏敏感信息) */
156
+ logInput?: boolean;
157
+ /** 把 output 写到 log(默认 false) */
158
+ logOutput?: boolean;
159
+ }
160
+ /**
161
+ * 一体化可观测性中间件:自动包住 procedure 执行,产出 log + trace + metrics。
162
+ *
163
+ * 用法(作为全局中间件):
164
+ * const deps = { logger, tracer, metrics };
165
+ * defineProcedure({ middleware: [observability(deps)], ... });
166
+ *
167
+ * 但更常见是放到 router 共享,或框架未来的 App.use 全局挂载。
168
+ */
169
+ declare function observability(deps: ObservabilityDeps, options?: ObservabilityOptions): Middleware;
170
+
171
+ interface ObservabilityFactoryOptions {
172
+ logger?: CreateLoggerOptions;
173
+ tracer?: CreateTracerOptions;
174
+ metrics?: CreateMetricsOptions;
175
+ middleware?: ObservabilityOptions;
176
+ }
177
+ interface ObservabilityFactory extends ObservabilityDeps {
178
+ /** 一体化中间件:放进 procedure.middleware 即生效 */
179
+ middleware: Middleware;
180
+ }
181
+ /**
182
+ * 一站式工厂:创建好 logger / tracer / metrics 三件套,并产出对应中间件。
183
+ *
184
+ * 三个子模块都是 peer-optional:
185
+ * - 装了 pino → 走 pino;否则走 consoleLogger
186
+ * - 装了 @opentelemetry/api → 走 OTel;否则 no-op
187
+ * - 装了 prom-client → 走 prom;否则 no-op,render() 返回空字符串
188
+ *
189
+ * 用法:
190
+ * const obs = await createObservability({
191
+ * logger: { level: 'info', bindings: { service: 'orders' } },
192
+ * metrics: { prefix: 'myapp' },
193
+ * });
194
+ * defineProcedure({ middleware: [obs.middleware], ... });
195
+ *
196
+ * // HTTP 暴露 /metrics
197
+ * app.get('/_metrics', async (req, reply) => {
198
+ * reply.header('content-type', 'text/plain; version=0.0.4');
199
+ * return obs.metrics.render();
200
+ * });
201
+ */
202
+ declare function createObservability(options?: ObservabilityFactoryOptions): Promise<ObservabilityFactory>;
203
+
204
+ export { type Counter, type CreateLoggerOptions, type CreateMetricsOptions, type CreateTracerOptions, type Histogram, type Metrics, type ObservabilityDeps, type ObservabilityFactory, type ObservabilityFactoryOptions, type ObservabilityOptions, SPAN_STATUS_ERROR, SPAN_STATUS_OK, type Span, type Tracer, createLogger, createMetrics, createObservability, createTracer, noopMetrics, noopTracer, observability };
package/dist/index.js ADDED
@@ -0,0 +1,264 @@
1
+ import { consoleLogger, OmniError } from '@omni-api/core';
2
+
3
+ // src/logger.ts
4
+
5
+ // src/optional-require.ts
6
+ async function tryRequire(moduleId) {
7
+ try {
8
+ const mod = await import(
9
+ /* @vite-ignore */
10
+ moduleId
11
+ );
12
+ return mod;
13
+ } catch {
14
+ return void 0;
15
+ }
16
+ }
17
+
18
+ // src/logger.ts
19
+ var DEFAULT_REDACT_PATHS = [
20
+ // Header 相关
21
+ "req.headers.authorization",
22
+ 'req.headers["x-api-key"]',
23
+ "req.headers.cookie",
24
+ "request.headers.authorization",
25
+ 'request.headers["x-api-key"]',
26
+ "request.headers.cookie",
27
+ "headers.authorization",
28
+ 'headers["x-api-key"]',
29
+ "headers.cookie",
30
+ // 业务字段
31
+ "*.password",
32
+ "*.passwd",
33
+ "*.secret",
34
+ "*.token",
35
+ "*.apiKey",
36
+ "*.api_key",
37
+ "*.privateKey",
38
+ "*.private_key",
39
+ "*.idCard",
40
+ "*.id_card",
41
+ "*.cvv",
42
+ "*.pin",
43
+ // 顶层(pino 不会递归 *.path 到顶层)
44
+ "password",
45
+ "token",
46
+ "secret",
47
+ "apiKey",
48
+ "privateKey"
49
+ ];
50
+ function adaptPino(p) {
51
+ const wrap = (level) => {
52
+ return (msg, meta) => {
53
+ if (meta) p[level](meta, msg);
54
+ else p[level]({}, msg);
55
+ };
56
+ };
57
+ return {
58
+ debug: wrap("debug"),
59
+ info: wrap("info"),
60
+ warn: wrap("warn"),
61
+ error: wrap("error"),
62
+ child: (extra) => adaptPino(p.child(extra))
63
+ };
64
+ }
65
+ async function createLogger(options = {}) {
66
+ const { level = "info", bindings = {}, pino: pinoInstance, redactPaths, redactCensor = "[REDACTED]" } = options;
67
+ if (pinoInstance) return adaptPino(pinoInstance);
68
+ const mod = await tryRequire("pino");
69
+ if (mod) {
70
+ const factory = mod.default ?? mod;
71
+ let finalPaths;
72
+ if (redactPaths === false) {
73
+ finalPaths = void 0;
74
+ } else if (Array.isArray(redactPaths)) {
75
+ finalPaths = [...DEFAULT_REDACT_PATHS, ...redactPaths];
76
+ } else {
77
+ finalPaths = DEFAULT_REDACT_PATHS;
78
+ }
79
+ const pinoOpts = { level, base: bindings };
80
+ if (finalPaths && finalPaths.length > 0) {
81
+ pinoOpts.redact = { paths: finalPaths, censor: redactCensor };
82
+ }
83
+ const instance = factory(pinoOpts);
84
+ return adaptPino(instance);
85
+ }
86
+ return Object.keys(bindings).length > 0 ? consoleLogger.child(bindings) : consoleLogger;
87
+ }
88
+
89
+ // src/tracer.ts
90
+ var SPAN_STATUS_OK = 1;
91
+ var SPAN_STATUS_ERROR = 2;
92
+ function adaptOtelTracer(otelTracer) {
93
+ return {
94
+ startActiveSpan: (name, attributes, fn) => {
95
+ return new Promise((resolve, reject) => {
96
+ otelTracer.startActiveSpan(name, async (rawSpan) => {
97
+ const span = rawSpan;
98
+ for (const [k, v] of Object.entries(attributes)) {
99
+ if (v !== void 0 && v !== null) span.setAttribute(k, v);
100
+ }
101
+ try {
102
+ const result = await fn({
103
+ setAttribute: (k, v) => span.setAttribute(k, v),
104
+ setStatus: (s) => span.setStatus(s),
105
+ recordException: (e) => span.recordException(e),
106
+ end: () => span.end()
107
+ });
108
+ span.setStatus({ code: SPAN_STATUS_OK });
109
+ span.end();
110
+ resolve(result);
111
+ } catch (err) {
112
+ span.recordException(err);
113
+ span.setStatus({
114
+ code: SPAN_STATUS_ERROR,
115
+ message: err instanceof Error ? err.message : String(err)
116
+ });
117
+ span.end();
118
+ reject(err);
119
+ }
120
+ });
121
+ });
122
+ }
123
+ };
124
+ }
125
+ var noopTracer = {
126
+ startActiveSpan: async (_name, _attrs, fn) => {
127
+ return fn({
128
+ setAttribute: () => void 0,
129
+ setStatus: () => void 0,
130
+ recordException: () => void 0,
131
+ end: () => void 0
132
+ });
133
+ }
134
+ };
135
+ async function createTracer(options = {}) {
136
+ if (options.tracer) return adaptOtelTracer(options.tracer);
137
+ const otel = await tryRequire("@opentelemetry/api");
138
+ if (otel?.trace) {
139
+ globalThis.__OMNI_OTEL_API__ = otel;
140
+ const tracer = otel.trace.getTracer(options.name ?? "omni-api");
141
+ return adaptOtelTracer(tracer);
142
+ }
143
+ return noopTracer;
144
+ }
145
+
146
+ // src/metrics.ts
147
+ var noopCounter = { inc: () => void 0 };
148
+ var noopHistogram = { observe: () => void 0 };
149
+ var noopMetrics = {
150
+ procedureTotal: noopCounter,
151
+ procedureDuration: noopHistogram,
152
+ render: async () => ""
153
+ };
154
+ var DEFAULT_BUCKETS = [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
155
+ async function createMetrics(options = {}) {
156
+ const prefix = options.prefix ?? "omni";
157
+ const buckets = options.buckets ?? DEFAULT_BUCKETS;
158
+ const promClient = options.promClient ?? await tryRequire("prom-client");
159
+ if (!promClient) return noopMetrics;
160
+ const registry = new promClient.Registry();
161
+ const procedureTotal = new promClient.Counter({
162
+ name: `${prefix}_procedure_total`,
163
+ help: "Total number of procedure invocations",
164
+ labelNames: ["name", "source", "status", "code"],
165
+ registers: [registry]
166
+ });
167
+ const procedureDuration = new promClient.Histogram({
168
+ name: `${prefix}_procedure_duration_seconds`,
169
+ help: "Procedure execution duration in seconds",
170
+ labelNames: ["name", "source", "status"],
171
+ buckets,
172
+ registers: [registry]
173
+ });
174
+ return {
175
+ procedureTotal,
176
+ procedureDuration,
177
+ render: () => registry.metrics()
178
+ };
179
+ }
180
+ function observability(deps, options = {}) {
181
+ const { logger: rootLogger, tracer, metrics } = deps;
182
+ const logSuccess = options.logSuccess ?? true;
183
+ const logInput = options.logInput ?? false;
184
+ const logOutput = options.logOutput ?? false;
185
+ return async (ctx, next) => {
186
+ const procName = ctx.procedure?.name ?? "unknown";
187
+ const source = ctx.source;
188
+ const start = process.hrtime.bigint();
189
+ const childLogger = rootLogger.child({ procedure: procName, source, requestId: ctx.requestId });
190
+ ctx.logger = childLogger;
191
+ return tracer.startActiveSpan(
192
+ `procedure ${procName}`,
193
+ {
194
+ "omni.procedure.name": procName,
195
+ "omni.source": source,
196
+ "omni.request_id": ctx.requestId,
197
+ "omni.user_id": ctx.user?.id ?? ""
198
+ },
199
+ async (span) => {
200
+ try {
201
+ if (logInput) {
202
+ childLogger.debug("procedure.start", { procedure: procName });
203
+ }
204
+ const result = await next();
205
+ const durationSec = Number(process.hrtime.bigint() - start) / 1e9;
206
+ metrics.procedureDuration.observe(
207
+ { name: procName, source, status: "success" },
208
+ durationSec
209
+ );
210
+ metrics.procedureTotal.inc({ name: procName, source, status: "success", code: "OK" });
211
+ span.setAttribute("omni.duration_ms", Math.round(durationSec * 1e3));
212
+ if (logSuccess) {
213
+ childLogger.info("procedure.success", {
214
+ durationMs: Math.round(durationSec * 1e3),
215
+ ...logOutput ? { output: safeStringify(result) } : {}
216
+ });
217
+ }
218
+ return result;
219
+ } catch (err) {
220
+ const durationSec = Number(process.hrtime.bigint() - start) / 1e9;
221
+ const code = err instanceof OmniError ? err.code : "INTERNAL";
222
+ const status = err instanceof OmniError && err.status < 500 ? "client_error" : "server_error";
223
+ metrics.procedureDuration.observe({ name: procName, source, status }, durationSec);
224
+ metrics.procedureTotal.inc({ name: procName, source, status, code });
225
+ span.setAttribute("omni.error.code", code);
226
+ span.setAttribute("omni.duration_ms", Math.round(durationSec * 1e3));
227
+ const isClientError = err instanceof OmniError && err.status < 500;
228
+ const logger = isClientError ? childLogger.warn : childLogger.error;
229
+ logger.call(childLogger, "procedure.failure", {
230
+ code,
231
+ durationMs: Math.round(durationSec * 1e3),
232
+ errorMessage: err instanceof Error ? err.message : String(err)
233
+ });
234
+ throw err;
235
+ }
236
+ }
237
+ );
238
+ };
239
+ }
240
+ function safeStringify(value, maxBytes = 4096) {
241
+ try {
242
+ const s = JSON.stringify(value);
243
+ if (s.length <= maxBytes) return s;
244
+ return s.slice(0, maxBytes) + "...[truncated]";
245
+ } catch {
246
+ return "[unserializable]";
247
+ }
248
+ }
249
+
250
+ // src/factory.ts
251
+ async function createObservability(options = {}) {
252
+ const [logger, tracer, metrics] = await Promise.all([
253
+ createLogger(options.logger),
254
+ createTracer(options.tracer),
255
+ createMetrics(options.metrics)
256
+ ]);
257
+ const deps = { logger, tracer, metrics };
258
+ const middleware = observability(deps, options.middleware);
259
+ return { ...deps, middleware };
260
+ }
261
+
262
+ export { SPAN_STATUS_ERROR, SPAN_STATUS_OK, createLogger, createMetrics, createObservability, createTracer, noopMetrics, noopTracer, observability };
263
+ //# sourceMappingURL=index.js.map
264
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/optional-require.ts","../src/logger.ts","../src/tracer.ts","../src/metrics.ts","../src/middleware.ts","../src/factory.ts"],"names":[],"mappings":";;;;;AAUA,eAAsB,WAAwB,QAAA,EAA0C;AACtF,EAAA,IAAI;AACF,IAAA,MAAM,MAAO,MAAM;AAAA;AAAA,MAA0B;AAAA,KAAA;AAC7C,IAAA,OAAO,GAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA;AAAA,EACT;AACF;;;ACyBA,IAAM,oBAAA,GAAuB;AAAA;AAAA,EAE3B,2BAAA;AAAA,EACA,0BAAA;AAAA,EACA,oBAAA;AAAA,EACA,+BAAA;AAAA,EACA,8BAAA;AAAA,EACA,wBAAA;AAAA,EACA,uBAAA;AAAA,EACA,sBAAA;AAAA,EACA,gBAAA;AAAA;AAAA,EAEA,YAAA;AAAA,EACA,UAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACA,WAAA;AAAA,EACA,cAAA;AAAA,EACA,eAAA;AAAA,EACA,UAAA;AAAA,EACA,WAAA;AAAA,EACA,OAAA;AAAA,EACA,OAAA;AAAA;AAAA,EAEA,UAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA;AAKA,SAAS,UAAU,CAAA,EAAqB;AACtC,EAAA,MAAM,IAAA,GAAO,CAAC,KAAA,KAA0E;AACtF,IAAA,OAAO,CAAC,KAAK,IAAA,KAAS;AACpB,MAAA,IAAI,IAAA,EAAM,CAAA,CAAE,KAAK,CAAA,CAAE,MAAM,GAAG,CAAA;AAAA,WACvB,CAAA,CAAE,KAAK,CAAA,CAAE,IAAI,GAAG,CAAA;AAAA,IACvB,CAAA;AAAA,EACF,CAAA;AACA,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,KAAK,OAAO,CAAA;AAAA,IACnB,IAAA,EAAM,KAAK,MAAM,CAAA;AAAA,IACjB,IAAA,EAAM,KAAK,MAAM,CAAA;AAAA,IACjB,KAAA,EAAO,KAAK,OAAO,CAAA;AAAA,IACnB,OAAO,CAAC,KAAA,KAAU,UAAU,CAAA,CAAE,KAAA,CAAM,KAAK,CAAC;AAAA,GAC5C;AACF;AAWA,eAAsB,YAAA,CAAa,OAAA,GAA+B,EAAC,EAAoB;AACrF,EAAA,MAAM,EAAE,KAAA,GAAQ,MAAA,EAAQ,QAAA,GAAW,EAAC,EAAG,IAAA,EAAM,YAAA,EAAc,WAAA,EAAa,YAAA,GAAe,YAAA,EAAa,GAAI,OAAA;AAGxG,EAAA,IAAI,YAAA,EAAc,OAAO,SAAA,CAAU,YAAY,CAAA;AAG/C,EAAA,MAAM,GAAA,GAAM,MAAM,UAAA,CAAuF,MAAM,CAAA;AAC/G,EAAA,IAAI,GAAA,EAAK;AACP,IAAA,MAAM,OAAA,GACH,IAAmD,OAAA,IACnD,GAAA;AAGH,IAAA,IAAI,UAAA;AACJ,IAAA,IAAI,gBAAgB,KAAA,EAAO;AACzB,MAAA,UAAA,GAAa,MAAA;AAAA,IACf,CAAA,MAAA,IAAW,KAAA,CAAM,OAAA,CAAQ,WAAW,CAAA,EAAG;AACrC,MAAA,UAAA,GAAa,CAAC,GAAG,oBAAA,EAAsB,GAAG,WAAW,CAAA;AAAA,IACvD,CAAA,MAAO;AACL,MAAA,UAAA,GAAa,oBAAA;AAAA,IACf;AAEA,IAAA,MAAM,QAAA,GAAoC,EAAE,KAAA,EAAO,IAAA,EAAM,QAAA,EAAS;AAClE,IAAA,IAAI,UAAA,IAAc,UAAA,CAAW,MAAA,GAAS,CAAA,EAAG;AACvC,MAAA,QAAA,CAAS,MAAA,GAAS,EAAE,KAAA,EAAO,UAAA,EAAY,QAAQ,YAAA,EAAa;AAAA,IAC9D;AAEA,IAAA,MAAM,QAAA,GAAW,QAAQ,QAAQ,CAAA;AACjC,IAAA,OAAO,UAAU,QAAQ,CAAA;AAAA,EAC3B;AAGA,EAAA,OAAO,MAAA,CAAO,KAAK,QAAQ,CAAA,CAAE,SAAS,CAAA,GAAI,aAAA,CAAc,KAAA,CAAM,QAAQ,CAAA,GAAI,aAAA;AAC5E;;;ACnHA,IAAM,cAAA,GAAiB;AACvB,IAAM,iBAAA,GAAoB;AAG1B,SAAS,gBAAgB,UAAA,EAEd;AACT,EAAA,OAAO;AAAA,IACL,eAAA,EAAiB,CAAI,IAAA,EAAc,UAAA,EAAqC,EAAA,KAA4C;AAClH,MAAA,OAAO,IAAI,OAAA,CAAW,CAAC,OAAA,EAAS,MAAA,KAAW;AACzC,QAAA,UAAA,CAAW,eAAA,CAAgB,IAAA,EAAM,OAAO,OAAA,KAAY;AAClD,UAAA,MAAM,IAAA,GAAO,OAAA;AAOb,UAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,UAAU,CAAA,EAAG;AAC/C,YAAA,IAAI,MAAM,MAAA,IAAa,CAAA,KAAM,MAAM,IAAA,CAAK,YAAA,CAAa,GAAG,CAAC,CAAA;AAAA,UAC3D;AACA,UAAA,IAAI;AACF,YAAA,MAAM,MAAA,GAAS,MAAM,EAAA,CAAG;AAAA,cACtB,cAAc,CAAC,CAAA,EAAG,MAAM,IAAA,CAAK,YAAA,CAAa,GAAG,CAAC,CAAA;AAAA,cAC9C,SAAA,EAAW,CAAC,CAAA,KAAM,IAAA,CAAK,UAAU,CAAC,CAAA;AAAA,cAClC,eAAA,EAAiB,CAAC,CAAA,KAAM,IAAA,CAAK,gBAAgB,CAAC,CAAA;AAAA,cAC9C,GAAA,EAAK,MAAM,IAAA,CAAK,GAAA;AAAI,aACrB,CAAA;AACD,YAAA,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAM,cAAA,EAAgB,CAAA;AACvC,YAAA,IAAA,CAAK,GAAA,EAAI;AACT,YAAA,OAAA,CAAQ,MAAM,CAAA;AAAA,UAChB,SAAS,GAAA,EAAK;AACZ,YAAA,IAAA,CAAK,gBAAgB,GAAG,CAAA;AACxB,YAAA,IAAA,CAAK,SAAA,CAAU;AAAA,cACb,IAAA,EAAM,iBAAA;AAAA,cACN,SAAS,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG;AAAA,aACzD,CAAA;AACD,YAAA,IAAA,CAAK,GAAA,EAAI;AACT,YAAA,MAAA,CAAO,GAAG,CAAA;AAAA,UACZ;AAAA,QACF,CAAC,CAAA;AAAA,MACH,CAAC,CAAA;AAAA,IACH;AAAA,GACF;AACF;AAGA,IAAM,UAAA,GAAqB;AAAA,EACzB,eAAA,EAAiB,OAAO,KAAA,EAAO,MAAA,EAAQ,EAAA,KAAO;AAC5C,IAAA,OAAO,EAAA,CAAG;AAAA,MACR,cAAc,MAAM,MAAA;AAAA,MACpB,WAAW,MAAM,MAAA;AAAA,MACjB,iBAAiB,MAAM,MAAA;AAAA,MACvB,KAAK,MAAM;AAAA,KACZ,CAAA;AAAA,EACH;AACF;AAeA,eAAsB,YAAA,CAAa,OAAA,GAA+B,EAAC,EAAoB;AACrF,EAAA,IAAI,OAAA,CAAQ,MAAA,EAAQ,OAAO,eAAA,CAAgB,QAAQ,MAAM,CAAA;AAEzD,EAAA,MAAM,IAAA,GAAO,MAAM,UAAA,CAEhB,oBAAoB,CAAA;AAEvB,EAAA,IAAI,MAAM,KAAA,EAAO;AAGf,IAAC,WAAmB,iBAAA,GAAoB,IAAA;AACxC,IAAA,MAAM,SAAS,IAAA,CAAK,KAAA,CAAM,SAAA,CAAU,OAAA,CAAQ,QAAQ,UAAU,CAAA;AAC9D,IAAA,OAAO,gBAAgB,MAAM,CAAA;AAAA,EAC/B;AAEA,EAAA,OAAO,UAAA;AACT;;;ACjEA,IAAM,WAAA,GAAuB,EAAE,GAAA,EAAK,MAAM,MAAA,EAAU;AACpD,IAAM,aAAA,GAA2B,EAAE,OAAA,EAAS,MAAM,MAAA,EAAU;AAC5D,IAAM,WAAA,GAAuB;AAAA,EAC3B,cAAA,EAAgB,WAAA;AAAA,EAChB,iBAAA,EAAmB,aAAA;AAAA,EACnB,QAAQ,YAAY;AACtB;AAWA,IAAM,eAAA,GAAkB,CAAC,IAAA,EAAO,IAAA,EAAO,IAAA,EAAM,KAAA,EAAO,IAAA,EAAM,GAAA,EAAK,IAAA,EAAM,GAAA,EAAK,CAAA,EAAG,GAAA,EAAK,GAAG,EAAE,CAAA;AASvF,eAAsB,aAAA,CAAc,OAAA,GAAgC,EAAC,EAAqB;AACxF,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,MAAA;AACjC,EAAA,MAAM,OAAA,GAAU,QAAQ,OAAA,IAAW,eAAA;AACnC,EAAA,MAAM,UAAA,GACJ,OAAA,CAAQ,UAAA,IAAe,MAAM,WAA2B,aAAa,CAAA;AAEvE,EAAA,IAAI,CAAC,YAAY,OAAO,WAAA;AAExB,EAAA,MAAM,QAAA,GAAW,IAAI,UAAA,CAAW,QAAA,EAAS;AACzC,EAAA,MAAM,cAAA,GAAiB,IAAI,UAAA,CAAW,OAAA,CAAQ;AAAA,IAC5C,IAAA,EAAM,GAAG,MAAM,CAAA,gBAAA,CAAA;AAAA,IACf,IAAA,EAAM,uCAAA;AAAA,IACN,UAAA,EAAY,CAAC,MAAA,EAAQ,QAAA,EAAU,UAAU,MAAM,CAAA;AAAA,IAC/C,SAAA,EAAW,CAAC,QAAQ;AAAA,GACrB,CAAA;AACD,EAAA,MAAM,iBAAA,GAAoB,IAAI,UAAA,CAAW,SAAA,CAAU;AAAA,IACjD,IAAA,EAAM,GAAG,MAAM,CAAA,2BAAA,CAAA;AAAA,IACf,IAAA,EAAM,yCAAA;AAAA,IACN,UAAA,EAAY,CAAC,MAAA,EAAQ,QAAA,EAAU,QAAQ,CAAA;AAAA,IACvC,OAAA;AAAA,IACA,SAAA,EAAW,CAAC,QAAQ;AAAA,GACrB,CAAA;AAED,EAAA,OAAO;AAAA,IACL,cAAA;AAAA,IACA,iBAAA;AAAA,IACA,MAAA,EAAQ,MAAO,QAAA,CAA4C,OAAA;AAAQ,GACrE;AACF;ACpEO,SAAS,aAAA,CACd,IAAA,EACA,OAAA,GAAgC,EAAC,EACrB;AACZ,EAAA,MAAM,EAAE,MAAA,EAAQ,UAAA,EAAY,MAAA,EAAQ,SAAQ,GAAI,IAAA;AAChD,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,IAAA;AACzC,EAAA,MAAM,QAAA,GAAW,QAAQ,QAAA,IAAY,KAAA;AACrC,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,KAAA;AAEvC,EAAA,OAAO,OAAO,KAAK,IAAA,KAAS;AAC1B,IAAA,MAAM,QAAA,GAAW,GAAA,CAAI,SAAA,EAAW,IAAA,IAAQ,SAAA;AACxC,IAAA,MAAM,SAAS,GAAA,CAAI,MAAA;AACnB,IAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,MAAA,CAAO,MAAA,EAAO;AAIpC,IAAA,MAAM,WAAA,GAAc,UAAA,CAAW,KAAA,CAAM,EAAE,SAAA,EAAW,UAAU,MAAA,EAAQ,SAAA,EAAW,GAAA,CAAI,SAAA,EAAW,CAAA;AAC9F,IAAC,IAA2B,MAAA,GAAS,WAAA;AAErC,IAAA,OAAO,MAAA,CAAO,eAAA;AAAA,MACZ,aAAa,QAAQ,CAAA,CAAA;AAAA,MACrB;AAAA,QACE,qBAAA,EAAuB,QAAA;AAAA,QACvB,aAAA,EAAe,MAAA;AAAA,QACf,mBAAmB,GAAA,CAAI,SAAA;AAAA,QACvB,cAAA,EAAgB,GAAA,CAAI,IAAA,EAAM,EAAA,IAAM;AAAA,OAClC;AAAA,MACA,OAAO,IAAA,KAAS;AACd,QAAA,IAAI;AACF,UAAA,IAAI,QAAA,EAAU;AACZ,YAAA,WAAA,CAAY,KAAA,CAAM,iBAAA,EAAmB,EAAE,SAAA,EAAW,UAAU,CAAA;AAAA,UAC9D;AAEA,UAAA,MAAM,MAAA,GAAS,MAAM,IAAA,EAAK;AAC1B,UAAA,MAAM,cAAc,MAAA,CAAO,OAAA,CAAQ,OAAO,MAAA,EAAO,GAAI,KAAK,CAAA,GAAI,GAAA;AAE9D,UAAA,OAAA,CAAQ,iBAAA,CAAkB,OAAA;AAAA,YACxB,EAAE,IAAA,EAAM,QAAA,EAAU,MAAA,EAAQ,QAAQ,SAAA,EAAU;AAAA,YAC5C;AAAA,WACF;AACA,UAAA,OAAA,CAAQ,cAAA,CAAe,GAAA,CAAI,EAAE,IAAA,EAAM,QAAA,EAAU,QAAQ,MAAA,EAAQ,SAAA,EAAW,IAAA,EAAM,IAAA,EAAM,CAAA;AAEpF,UAAA,IAAA,CAAK,aAAa,kBAAA,EAAoB,IAAA,CAAK,KAAA,CAAM,WAAA,GAAc,GAAI,CAAC,CAAA;AAEpE,UAAA,IAAI,UAAA,EAAY;AACd,YAAA,WAAA,CAAY,KAAK,mBAAA,EAAqB;AAAA,cACpC,UAAA,EAAY,IAAA,CAAK,KAAA,CAAM,WAAA,GAAc,GAAI,CAAA;AAAA,cACzC,GAAI,YAAY,EAAE,MAAA,EAAQ,cAAc,MAAM,CAAA,KAAM;AAAC,aACtD,CAAA;AAAA,UACH;AAEA,UAAA,OAAO,MAAA;AAAA,QACT,SAAS,GAAA,EAAK;AACZ,UAAA,MAAM,cAAc,MAAA,CAAO,OAAA,CAAQ,OAAO,MAAA,EAAO,GAAI,KAAK,CAAA,GAAI,GAAA;AAC9D,UAAA,MAAM,IAAA,GAAO,GAAA,YAAe,SAAA,GAAY,GAAA,CAAI,IAAA,GAAO,UAAA;AACnD,UAAA,MAAM,SAAS,GAAA,YAAe,SAAA,IAAa,GAAA,CAAI,MAAA,GAAS,MAAM,cAAA,GAAiB,cAAA;AAE/E,UAAA,OAAA,CAAQ,iBAAA,CAAkB,QAAQ,EAAE,IAAA,EAAM,UAAU,MAAA,EAAQ,MAAA,IAAU,WAAW,CAAA;AACjF,UAAA,OAAA,CAAQ,cAAA,CAAe,IAAI,EAAE,IAAA,EAAM,UAAU,MAAA,EAAQ,MAAA,EAAQ,MAAM,CAAA;AAEnE,UAAA,IAAA,CAAK,YAAA,CAAa,mBAAmB,IAAI,CAAA;AACzC,UAAA,IAAA,CAAK,aAAa,kBAAA,EAAoB,IAAA,CAAK,KAAA,CAAM,WAAA,GAAc,GAAI,CAAC,CAAA;AAGpE,UAAA,MAAM,aAAA,GAAgB,GAAA,YAAe,SAAA,IAAa,GAAA,CAAI,MAAA,GAAS,GAAA;AAC/D,UAAA,MAAM,MAAA,GAAS,aAAA,GAAgB,WAAA,CAAY,IAAA,GAAO,WAAA,CAAY,KAAA;AAC9D,UAAA,MAAA,CAAO,IAAA,CAAK,aAAa,mBAAA,EAAqB;AAAA,YAC5C,IAAA;AAAA,YACA,UAAA,EAAY,IAAA,CAAK,KAAA,CAAM,WAAA,GAAc,GAAI,CAAA;AAAA,YACzC,cAAc,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG;AAAA,WAC9D,CAAA;AAED,UAAA,MAAM,GAAA;AAAA,QACR;AAAA,MACF;AAAA,KACF;AAAA,EACF,CAAA;AACF;AAGA,SAAS,aAAA,CAAc,KAAA,EAAgB,QAAA,GAAW,IAAA,EAAc;AAC9D,EAAA,IAAI;AACF,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,SAAA,CAAU,KAAK,CAAA;AAC9B,IAAA,IAAI,CAAA,CAAE,MAAA,IAAU,QAAA,EAAU,OAAO,CAAA;AACjC,IAAA,OAAO,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,QAAQ,CAAA,GAAI,gBAAA;AAAA,EAChC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,kBAAA;AAAA,EACT;AACF;;;AC9EA,eAAsB,mBAAA,CACpB,OAAA,GAAuC,EAAC,EACT;AAC/B,EAAA,MAAM,CAAC,MAAA,EAAQ,MAAA,EAAQ,OAAO,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,IAClD,YAAA,CAAa,QAAQ,MAAM,CAAA;AAAA,IAC3B,YAAA,CAAa,QAAQ,MAAM,CAAA;AAAA,IAC3B,aAAA,CAAc,QAAQ,OAAO;AAAA,GAC9B,CAAA;AAED,EAAA,MAAM,IAAA,GAA0B,EAAE,MAAA,EAAQ,MAAA,EAAQ,OAAA,EAAQ;AAC1D,EAAA,MAAM,UAAA,GAAa,aAAA,CAAc,IAAA,EAAM,OAAA,CAAQ,UAAU,CAAA;AAEzD,EAAA,OAAO,EAAE,GAAG,IAAA,EAAM,UAAA,EAAW;AAC/B","file":"index.js","sourcesContent":["/**\n * 动态加载可选 peer dependency。\n *\n * 设计:避免静态 `import` 让 pino/OTel/prom-client 成为强依赖。\n * 业务方装了哪个,就启用哪个;都没装则自动 no-op。\n *\n * 使用:\n * const pino = await tryRequire<typeof import('pino')>('pino');\n * if (pino) { ... } else { // fallback }\n */\nexport async function tryRequire<T = unknown>(moduleId: string): Promise<T | undefined> {\n try {\n const mod = (await import(/* @vite-ignore */ moduleId)) as T;\n return mod;\n } catch {\n return undefined;\n }\n}\n\n/**\n * 同步版本:用 createRequire 加载(仅 CommonJS 包可用)。\n *\n * pino / prom-client 都支持 CJS interop,所以这条路可用;\n * 但如果用户用纯 ESM 项目装的纯 ESM 版本,会失败 —— 此时 fallback 到动态 import 异步加载即可。\n */\nexport function tryRequireSync<T = unknown>(moduleId: string): T | undefined {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n return require(moduleId) as T;\n } catch {\n return undefined;\n }\n}\n","import { type Logger, consoleLogger } from '@omni-api/core';\nimport { tryRequire } from './optional-require.js';\n\n/** pino 的最小接口(避免引入完整类型) */\ninterface PinoLike {\n trace(obj: unknown, msg?: string): void;\n debug(obj: unknown, msg?: string): void;\n info(obj: unknown, msg?: string): void;\n warn(obj: unknown, msg?: string): void;\n error(obj: unknown, msg?: string): void;\n child(bindings: Record<string, unknown>): PinoLike;\n}\n\nexport interface CreateLoggerOptions {\n /** 日志级别 */\n level?: 'trace' | 'debug' | 'info' | 'warn' | 'error';\n /** 全局绑定字段(如 service / version / env) */\n bindings?: Record<string, unknown>;\n /**\n * 直接传入 pino 实例(已存在的 logger 复用)。\n * 不传则尝试动态加载 pino;pino 不可用则 fallback 到 consoleLogger。\n */\n pino?: PinoLike;\n /**\n * 字段级脱敏路径(pino 原生支持的 path 语法)。\n *\n * 默认包含一组安全基线:authorization / cookie / token / password 等。\n * 设 `redactPaths: false` 关闭脱敏;传数组则在默认基线上叠加。\n */\n redactPaths?: string[] | false;\n /** 自定义替换值,默认 '[REDACTED]' */\n redactCensor?: string;\n}\n\n/**\n * 默认敏感字段路径(pino path 语法)。\n *\n * 覆盖最常见的\"日志泄漏 token\"场景:\n * - HTTP request headers(Bearer / API key / cookie)\n * - 业务字段:password / token / secret / privateKey / idCard / cvv\n * - 嵌套通配符 `*`:自动覆盖任意层级\n */\nconst DEFAULT_REDACT_PATHS = [\n // Header 相关\n 'req.headers.authorization',\n 'req.headers[\"x-api-key\"]',\n 'req.headers.cookie',\n 'request.headers.authorization',\n 'request.headers[\"x-api-key\"]',\n 'request.headers.cookie',\n 'headers.authorization',\n 'headers[\"x-api-key\"]',\n 'headers.cookie',\n // 业务字段\n '*.password',\n '*.passwd',\n '*.secret',\n '*.token',\n '*.apiKey',\n '*.api_key',\n '*.privateKey',\n '*.private_key',\n '*.idCard',\n '*.id_card',\n '*.cvv',\n '*.pin',\n // 顶层(pino 不会递归 *.path 到顶层)\n 'password',\n 'token',\n 'secret',\n 'apiKey',\n 'privateKey',\n];\n\n/**\n * 把 pino-like 对象适配为 OmniAPI Logger 接口。\n */\nfunction adaptPino(p: PinoLike): Logger {\n const wrap = (level: 'trace' | 'debug' | 'info' | 'warn' | 'error'): Logger['debug'] => {\n return (msg, meta) => {\n if (meta) p[level](meta, msg);\n else p[level]({}, msg);\n };\n };\n return {\n debug: wrap('debug'),\n info: wrap('info'),\n warn: wrap('warn'),\n error: wrap('error'),\n child: (extra) => adaptPino(p.child(extra)),\n };\n}\n\n/**\n * 创建结构化 Logger。\n *\n * - 优先级:options.pino > 动态加载 pino > consoleLogger fallback\n * - 异步加载 pino(peer-optional);为了让构造同步可用,提供了同步 fallback\n *\n * 业务通常这样用:\n * const logger = await createLogger({ level: 'info', bindings: { service: 'orders' } });\n */\nexport async function createLogger(options: CreateLoggerOptions = {}): Promise<Logger> {\n const { level = 'info', bindings = {}, pino: pinoInstance, redactPaths, redactCensor = '[REDACTED]' } = options;\n\n // 1) 显式注入\n if (pinoInstance) return adaptPino(pinoInstance);\n\n // 2) 动态加载 pino\n const mod = await tryRequire<{ default: (opts?: unknown) => PinoLike } | { (opts?: unknown): PinoLike }>('pino');\n if (mod) {\n const factory =\n (mod as { default?: (opts?: unknown) => PinoLike }).default ??\n (mod as (opts?: unknown) => PinoLike);\n\n // 计算最终 redact 路径\n let finalPaths: string[] | undefined;\n if (redactPaths === false) {\n finalPaths = undefined;\n } else if (Array.isArray(redactPaths)) {\n finalPaths = [...DEFAULT_REDACT_PATHS, ...redactPaths];\n } else {\n finalPaths = DEFAULT_REDACT_PATHS;\n }\n\n const pinoOpts: Record<string, unknown> = { level, base: bindings };\n if (finalPaths && finalPaths.length > 0) {\n pinoOpts.redact = { paths: finalPaths, censor: redactCensor };\n }\n\n const instance = factory(pinoOpts);\n return adaptPino(instance);\n }\n\n // 3) Fallback:consoleLogger(带 bindings)\n return Object.keys(bindings).length > 0 ? consoleLogger.child(bindings) : consoleLogger;\n}\n","import { tryRequire } from './optional-require.js';\n\n/**\n * Span 接口(OTel API 的最小子集)—— 用最小约束避免强依赖整个 OTel 类型库。\n */\nexport interface Span {\n setAttribute(key: string, value: string | number | boolean): void;\n setStatus(status: { code: number; message?: string }): void;\n recordException(err: unknown): void;\n end(): void;\n}\n\n/**\n * Tracer 接口。\n */\nexport interface Tracer {\n /** 创建 span 并执行 fn,正常 / 异常都会自动 end */\n startActiveSpan<T>(name: string, attributes: Record<string, unknown>, fn: (span: Span) => Promise<T>): Promise<T>;\n}\n\n/** OTel SpanStatusCode 常量(避免引入 OTel 包) */\nconst SPAN_STATUS_OK = 1;\nconst SPAN_STATUS_ERROR = 2;\n\n/** 实现 OTel API 形态的 Tracer 适配 */\nfunction adaptOtelTracer(otelTracer: {\n startActiveSpan: (name: string, fn: (span: Record<string, unknown>) => unknown) => unknown;\n}): Tracer {\n return {\n startActiveSpan: <T>(name: string, attributes: Record<string, unknown>, fn: (s: Span) => Promise<T>): Promise<T> => {\n return new Promise<T>((resolve, reject) => {\n otelTracer.startActiveSpan(name, async (rawSpan) => {\n const span = rawSpan as Record<string, unknown> & {\n setAttribute(k: string, v: unknown): void;\n setStatus(s: { code: number; message?: string }): void;\n recordException(e: unknown): void;\n end(): void;\n };\n // 注入初始属性\n for (const [k, v] of Object.entries(attributes)) {\n if (v !== undefined && v !== null) span.setAttribute(k, v);\n }\n try {\n const result = await fn({\n setAttribute: (k, v) => span.setAttribute(k, v),\n setStatus: (s) => span.setStatus(s),\n recordException: (e) => span.recordException(e),\n end: () => span.end(),\n });\n span.setStatus({ code: SPAN_STATUS_OK });\n span.end();\n resolve(result);\n } catch (err) {\n span.recordException(err);\n span.setStatus({\n code: SPAN_STATUS_ERROR,\n message: err instanceof Error ? err.message : String(err),\n });\n span.end();\n reject(err);\n }\n });\n });\n },\n };\n}\n\n/** No-op Tracer:OTel 未安装时的兜底,保证业务代码不需要写 if 判断 */\nconst noopTracer: Tracer = {\n startActiveSpan: async (_name, _attrs, fn) => {\n return fn({\n setAttribute: () => undefined,\n setStatus: () => undefined,\n recordException: () => undefined,\n end: () => undefined,\n });\n },\n};\n\nexport interface CreateTracerOptions {\n /** Tracer 名称,默认 'omni-api' */\n name?: string;\n /** 直接注入 OTel TracerProvider 中拿到的 tracer(高级用法) */\n tracer?: { startActiveSpan: (name: string, fn: (s: Record<string, unknown>) => unknown) => unknown };\n}\n\n/**\n * 创建 Tracer。\n *\n * - 优先级:options.tracer > 动态加载 @opentelemetry/api > noopTracer\n * - OTel 未安装时返回 no-op,业务代码无需 if-else\n */\nexport async function createTracer(options: CreateTracerOptions = {}): Promise<Tracer> {\n if (options.tracer) return adaptOtelTracer(options.tracer);\n\n const otel = await tryRequire<{\n trace: { getTracer: (name: string) => { startActiveSpan: (name: string, fn: (s: Record<string, unknown>) => unknown) => unknown } };\n }>('@opentelemetry/api');\n\n if (otel?.trace) {\n // 把 OTel api 注册到全局,让 @omni-api/core 的 createTracedFetch 可以零依赖访问\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (globalThis as any).__OMNI_OTEL_API__ = otel;\n const tracer = otel.trace.getTracer(options.name ?? 'omni-api');\n return adaptOtelTracer(tracer);\n }\n\n return noopTracer;\n}\n\nexport { noopTracer, SPAN_STATUS_OK, SPAN_STATUS_ERROR };\n","import { tryRequire } from './optional-require.js';\n\n/**\n * 一个最小的 Counter 接口(避开 prom-client 完整类型)。\n */\nexport interface Counter {\n inc(labels?: Record<string, string | number>, value?: number): void;\n}\n\n/**\n * 一个最小的 Histogram 接口。\n */\nexport interface Histogram {\n observe(labels: Record<string, string | number>, value: number): void;\n}\n\n/**\n * Metrics 接口:\n * - procedureTotal: 累计调用数(按 status / source / name 维度)\n * - procedureDuration: 调用耗时分布(histogram,秒为单位)\n * - render(): 输出 Prometheus 文本(接 /_metrics 用)\n */\nexport interface Metrics {\n procedureTotal: Counter;\n procedureDuration: Histogram;\n /** 渲染 Prometheus exposition 文本 */\n render(): Promise<string>;\n}\n\ninterface PromClientLike {\n Counter: new (config: { name: string; help: string; labelNames: string[]; registers?: unknown[] }) => Counter;\n Histogram: new (config: {\n name: string;\n help: string;\n labelNames: string[];\n buckets?: number[];\n registers?: unknown[];\n }) => Histogram;\n Registry: new () => { metrics(): Promise<string>; clear(): void };\n register: { metrics(): Promise<string> };\n}\n\n/** No-op metrics(prom-client 未安装时的兜底) */\nconst noopCounter: Counter = { inc: () => undefined };\nconst noopHistogram: Histogram = { observe: () => undefined };\nconst noopMetrics: Metrics = {\n procedureTotal: noopCounter,\n procedureDuration: noopHistogram,\n render: async () => '',\n};\n\nexport interface CreateMetricsOptions {\n /** 指标名前缀,默认 'omni' */\n prefix?: string;\n /** Histogram 桶(秒),默认覆盖 1ms ~ 10s */\n buckets?: number[];\n /** 直接注入 prom-client(测试用) */\n promClient?: PromClientLike;\n}\n\nconst DEFAULT_BUCKETS = [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];\n\n/**\n * 创建 metrics 实例。\n *\n * - prom-client 未安装则返回 no-op\n * - 装了的话,自动注册两个指标到 **新建的独立 Registry**(避免和业务原有 register 冲突)\n * render() 输出该 Registry 的指标\n */\nexport async function createMetrics(options: CreateMetricsOptions = {}): Promise<Metrics> {\n const prefix = options.prefix ?? 'omni';\n const buckets = options.buckets ?? DEFAULT_BUCKETS;\n const promClient =\n options.promClient ?? (await tryRequire<PromClientLike>('prom-client'));\n\n if (!promClient) return noopMetrics;\n\n const registry = new promClient.Registry();\n const procedureTotal = new promClient.Counter({\n name: `${prefix}_procedure_total`,\n help: 'Total number of procedure invocations',\n labelNames: ['name', 'source', 'status', 'code'],\n registers: [registry],\n });\n const procedureDuration = new promClient.Histogram({\n name: `${prefix}_procedure_duration_seconds`,\n help: 'Procedure execution duration in seconds',\n labelNames: ['name', 'source', 'status'],\n buckets,\n registers: [registry],\n });\n\n return {\n procedureTotal,\n procedureDuration,\n render: () => (registry as { metrics(): Promise<string> }).metrics(),\n };\n}\n\nexport { noopMetrics };\n","import { OmniError, type Middleware } from '@omni-api/core';\nimport type { Logger } from '@omni-api/core';\nimport type { Metrics } from './metrics.js';\nimport type { Tracer } from './tracer.js';\n\nexport interface ObservabilityDeps {\n logger: Logger;\n tracer: Tracer;\n metrics: Metrics;\n}\n\nexport interface ObservabilityOptions {\n /** 是否在 info 级别打印每次成功调用(默认 true) */\n logSuccess?: boolean;\n /** 把 input 写到 log(默认 false,避免日志泄漏敏感信息) */\n logInput?: boolean;\n /** 把 output 写到 log(默认 false) */\n logOutput?: boolean;\n}\n\n/**\n * 一体化可观测性中间件:自动包住 procedure 执行,产出 log + trace + metrics。\n *\n * 用法(作为全局中间件):\n * const deps = { logger, tracer, metrics };\n * defineProcedure({ middleware: [observability(deps)], ... });\n *\n * 但更常见是放到 router 共享,或框架未来的 App.use 全局挂载。\n */\nexport function observability(\n deps: ObservabilityDeps,\n options: ObservabilityOptions = {},\n): Middleware {\n const { logger: rootLogger, tracer, metrics } = deps;\n const logSuccess = options.logSuccess ?? true;\n const logInput = options.logInput ?? false;\n const logOutput = options.logOutput ?? false;\n\n return async (ctx, next) => {\n const procName = ctx.procedure?.name ?? 'unknown';\n const source = ctx.source;\n const start = process.hrtime.bigint();\n\n // 把 logger 切换为 child logger(带 procedure name),覆盖原 ctx.logger\n // 这样下游中间件 / handler 打日志都自带 procedure 信息\n const childLogger = rootLogger.child({ procedure: procName, source, requestId: ctx.requestId });\n (ctx as { logger: Logger }).logger = childLogger;\n\n return tracer.startActiveSpan(\n `procedure ${procName}`,\n {\n 'omni.procedure.name': procName,\n 'omni.source': source,\n 'omni.request_id': ctx.requestId,\n 'omni.user_id': ctx.user?.id ?? '',\n },\n async (span) => {\n try {\n if (logInput) {\n childLogger.debug('procedure.start', { procedure: procName });\n }\n\n const result = await next();\n const durationSec = Number(process.hrtime.bigint() - start) / 1e9;\n\n metrics.procedureDuration.observe(\n { name: procName, source, status: 'success' },\n durationSec,\n );\n metrics.procedureTotal.inc({ name: procName, source, status: 'success', code: 'OK' });\n\n span.setAttribute('omni.duration_ms', Math.round(durationSec * 1000));\n\n if (logSuccess) {\n childLogger.info('procedure.success', {\n durationMs: Math.round(durationSec * 1000),\n ...(logOutput ? { output: safeStringify(result) } : {}),\n });\n }\n\n return result;\n } catch (err) {\n const durationSec = Number(process.hrtime.bigint() - start) / 1e9;\n const code = err instanceof OmniError ? err.code : 'INTERNAL';\n const status = err instanceof OmniError && err.status < 500 ? 'client_error' : 'server_error';\n\n metrics.procedureDuration.observe({ name: procName, source, status }, durationSec);\n metrics.procedureTotal.inc({ name: procName, source, status, code });\n\n span.setAttribute('omni.error.code', code);\n span.setAttribute('omni.duration_ms', Math.round(durationSec * 1000));\n\n // 5xx / 非 OmniError 走 error;4xx 走 warn(业务可预期)\n const isClientError = err instanceof OmniError && err.status < 500;\n const logger = isClientError ? childLogger.warn : childLogger.error;\n logger.call(childLogger, 'procedure.failure', {\n code,\n durationMs: Math.round(durationSec * 1000),\n errorMessage: err instanceof Error ? err.message : String(err),\n });\n\n throw err;\n }\n },\n );\n };\n}\n\n/** 序列化但限制大小,防止巨大对象写满日志 */\nfunction safeStringify(value: unknown, maxBytes = 4096): string {\n try {\n const s = JSON.stringify(value);\n if (s.length <= maxBytes) return s;\n return s.slice(0, maxBytes) + '...[truncated]';\n } catch {\n return '[unserializable]';\n }\n}\n","import { createLogger, type CreateLoggerOptions } from './logger.js';\nimport { createTracer, type CreateTracerOptions } from './tracer.js';\nimport { createMetrics, type CreateMetricsOptions } from './metrics.js';\nimport { observability, type ObservabilityDeps, type ObservabilityOptions } from './middleware.js';\nimport type { Middleware } from '@omni-api/core';\n\nexport interface ObservabilityFactoryOptions {\n logger?: CreateLoggerOptions;\n tracer?: CreateTracerOptions;\n metrics?: CreateMetricsOptions;\n middleware?: ObservabilityOptions;\n}\n\nexport interface ObservabilityFactory extends ObservabilityDeps {\n /** 一体化中间件:放进 procedure.middleware 即生效 */\n middleware: Middleware;\n}\n\n/**\n * 一站式工厂:创建好 logger / tracer / metrics 三件套,并产出对应中间件。\n *\n * 三个子模块都是 peer-optional:\n * - 装了 pino → 走 pino;否则走 consoleLogger\n * - 装了 @opentelemetry/api → 走 OTel;否则 no-op\n * - 装了 prom-client → 走 prom;否则 no-op,render() 返回空字符串\n *\n * 用法:\n * const obs = await createObservability({\n * logger: { level: 'info', bindings: { service: 'orders' } },\n * metrics: { prefix: 'myapp' },\n * });\n * defineProcedure({ middleware: [obs.middleware], ... });\n *\n * // HTTP 暴露 /metrics\n * app.get('/_metrics', async (req, reply) => {\n * reply.header('content-type', 'text/plain; version=0.0.4');\n * return obs.metrics.render();\n * });\n */\nexport async function createObservability(\n options: ObservabilityFactoryOptions = {},\n): Promise<ObservabilityFactory> {\n const [logger, tracer, metrics] = await Promise.all([\n createLogger(options.logger),\n createTracer(options.tracer),\n createMetrics(options.metrics),\n ]);\n\n const deps: ObservabilityDeps = { logger, tracer, metrics };\n const middleware = observability(deps, options.middleware);\n\n return { ...deps, middleware };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@omni-api/plugin-observability",
3
+ "version": "0.0.1",
4
+ "description": "Observability for OmniAPI: structured logger + OTel tracing + Prometheus metrics. All optional, all pluggable.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist", "README.md"],
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "typecheck": "tsc --noEmit",
21
+ "clean": "rm -rf dist .turbo coverage"
22
+ },
23
+ "dependencies": {
24
+ "@omni-api/core": "workspace:*"
25
+ },
26
+ "peerDependencies": {
27
+ "pino": "^9.0.0",
28
+ "@opentelemetry/api": "^1.9.0",
29
+ "prom-client": "^15.0.0"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "pino": { "optional": true },
33
+ "@opentelemetry/api": { "optional": true },
34
+ "prom-client": { "optional": true }
35
+ },
36
+ "devDependencies": {
37
+ "tsup": "^8.3.0",
38
+ "typescript": "^5.7.0",
39
+ "vitest": "^2.1.0",
40
+ "zod": "^3.23.8",
41
+ "@types/node": "^22.10.0"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }