@clyse/trace 0.1.0
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/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/index.cjs +165 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +49 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +140 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clyse
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @clyse/trace
|
|
2
|
+
|
|
3
|
+
Tiny, zero-dependency client for shipping logs to a [Trace](https://github.com/Clyse-Devs/Trace)
|
|
4
|
+
instance. Works in Node 18+ and modern browsers (uses the global `fetch`).
|
|
5
|
+
|
|
6
|
+
- ๐ฆ Zero dependencies, ESM + CJS + types
|
|
7
|
+
- ๐งต Trace correlation (`withTrace`, `child`)
|
|
8
|
+
- ๐ชฃ Buffered + batched, flushes on interval, size, and process exit / page unload
|
|
9
|
+
- ๐ Never throws โ transport errors go to `onError`, entries are dropped
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @clyse/trace
|
|
15
|
+
# or: bun add @clyse/trace
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createTrace } from '@clyse/trace';
|
|
22
|
+
|
|
23
|
+
const trace = createTrace({
|
|
24
|
+
url: process.env.TRACE_URL!, // e.g. "https://trace.clyse.studio/api" or "http://localhost:4010"
|
|
25
|
+
key: process.env.TRACE_KEY!, // application API key (trc_โฆ) from Trace settings
|
|
26
|
+
defaultMeta: { env: process.env.NODE_ENV }
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
trace.info('server started', { port: 3000 });
|
|
30
|
+
trace.error('payment failed', { order_id: 42 });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The application name is bound to the API key server-side, so you don't send it.
|
|
34
|
+
|
|
35
|
+
### Trace correlation
|
|
36
|
+
|
|
37
|
+
Tag every log of one request with the same id โ the dashboard groups them in a click.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
function handler(reqId: string) {
|
|
41
|
+
const log = trace.withTrace(reqId); // adds meta.trace_id
|
|
42
|
+
log.info('request received');
|
|
43
|
+
log.warn('slow query', { ms: 820 });
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`child` merges arbitrary meta into every entry:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const dbLog = trace.child({ component: 'db' });
|
|
51
|
+
dbLog.debug('pool acquired');
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Flushing
|
|
55
|
+
|
|
56
|
+
Entries are buffered and flushed automatically (every `flushInterval` ms, when the
|
|
57
|
+
buffer hits `batchSize`, and on exit/unload). Flush manually when you need to:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
await trace.flush(); // send now
|
|
61
|
+
await trace.close(); // flush and stop the timer (e.g. before a graceful shutdown)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Options
|
|
65
|
+
|
|
66
|
+
| Option | Default | Description |
|
|
67
|
+
| -------------- | -------- | ------------------------------------------------------------------ |
|
|
68
|
+
| `url` | โ | Base URL of the Trace API (with or without a trailing `/ingest`). |
|
|
69
|
+
| `key` | โ | Application API key (`trc_โฆ`). |
|
|
70
|
+
| `flushInterval`| `2000` | Max ms between automatic flushes. |
|
|
71
|
+
| `batchSize` | `50` | Flush as soon as the buffer reaches this many entries. |
|
|
72
|
+
| `maxBuffer` | `1000` | Hard cap on buffered entries; oldest dropped beyond it. |
|
|
73
|
+
| `defaultMeta` | โ | Meta merged into every entry. |
|
|
74
|
+
| `console` | `false` | Also mirror entries to the local console. |
|
|
75
|
+
| `onError` | no-op | Called on transport errors (never thrown). |
|
|
76
|
+
| `fetch` | global | Custom fetch (tests, proxies, older runtimes). |
|
|
77
|
+
| `flushOnExit` | `true` | Flush on process exit / page unload. |
|
|
78
|
+
|
|
79
|
+
## Publishing
|
|
80
|
+
|
|
81
|
+
Published to npm by the **Publish SDK** GitHub Action. To cut a release:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
cd packages/trace
|
|
85
|
+
npm version patch # bumps package.json (use minor/major as needed)
|
|
86
|
+
git commit -am "release sdk vX.Y.Z"
|
|
87
|
+
git tag sdk-vX.Y.Z # tag MUST match the new package.json version
|
|
88
|
+
git push && git push --tags
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The workflow tests, builds, checks the tag matches the version, and runs `npm publish`.
|
|
92
|
+
Requires an `NPM_TOKEN` repository secret (npm **Automation** token for the `@clyse` org).
|
|
93
|
+
The scope must match your npm org name โ rename the package if your org isn't `clyse`.
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
|
98
|
+
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createTrace: () => createTrace,
|
|
24
|
+
default: () => index_default
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var LEVELS = ["debug", "info", "warn", "error"];
|
|
28
|
+
function normalizeBase(url) {
|
|
29
|
+
const trimmed = url.replace(/\/+$/, "");
|
|
30
|
+
return trimmed.endsWith("/ingest") ? trimmed : `${trimmed}/ingest`;
|
|
31
|
+
}
|
|
32
|
+
function mergeMeta(a, b) {
|
|
33
|
+
if (!a && !b) return void 0;
|
|
34
|
+
const out = { ...a ?? {}, ...b ?? {} };
|
|
35
|
+
return Object.keys(out).length ? out : void 0;
|
|
36
|
+
}
|
|
37
|
+
var TraceCore = class {
|
|
38
|
+
endpoint;
|
|
39
|
+
key;
|
|
40
|
+
batchSize;
|
|
41
|
+
maxBuffer;
|
|
42
|
+
defaultMeta;
|
|
43
|
+
mirror;
|
|
44
|
+
onError;
|
|
45
|
+
fetchImpl;
|
|
46
|
+
buffer = [];
|
|
47
|
+
timer = null;
|
|
48
|
+
flushing = null;
|
|
49
|
+
closed = false;
|
|
50
|
+
constructor(opts) {
|
|
51
|
+
if (!opts.url) throw new Error("@clyse/trace: `url` is required");
|
|
52
|
+
if (!opts.key) throw new Error("@clyse/trace: `key` is required");
|
|
53
|
+
this.endpoint = normalizeBase(opts.url);
|
|
54
|
+
this.key = opts.key;
|
|
55
|
+
this.batchSize = opts.batchSize ?? 50;
|
|
56
|
+
this.maxBuffer = opts.maxBuffer ?? 1e3;
|
|
57
|
+
this.defaultMeta = opts.defaultMeta;
|
|
58
|
+
this.mirror = opts.console ?? false;
|
|
59
|
+
this.onError = opts.onError ?? (() => {
|
|
60
|
+
});
|
|
61
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
62
|
+
if (typeof f !== "function") {
|
|
63
|
+
throw new Error("@clyse/trace: no fetch available; pass options.fetch on older runtimes");
|
|
64
|
+
}
|
|
65
|
+
this.fetchImpl = f.bind(globalThis);
|
|
66
|
+
const interval = opts.flushInterval ?? 2e3;
|
|
67
|
+
this.timer = setInterval(() => void this.flush(), interval);
|
|
68
|
+
this.timer.unref?.();
|
|
69
|
+
if (opts.flushOnExit ?? true) this.registerExitHooks();
|
|
70
|
+
}
|
|
71
|
+
enqueue(level, message, meta) {
|
|
72
|
+
if (this.closed) return;
|
|
73
|
+
const lvl = LEVELS.includes(level) ? level : "info";
|
|
74
|
+
const entry = {
|
|
75
|
+
level: lvl,
|
|
76
|
+
message: typeof message === "string" ? message : String(message),
|
|
77
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
78
|
+
meta: mergeMeta(this.defaultMeta, meta)
|
|
79
|
+
};
|
|
80
|
+
if (this.mirror) this.mirrorToConsole(entry);
|
|
81
|
+
this.buffer.push(entry);
|
|
82
|
+
if (this.buffer.length > this.maxBuffer) {
|
|
83
|
+
this.buffer.splice(0, this.buffer.length - this.maxBuffer);
|
|
84
|
+
}
|
|
85
|
+
if (this.buffer.length >= this.batchSize) void this.flush();
|
|
86
|
+
}
|
|
87
|
+
async flush() {
|
|
88
|
+
if (this.flushing) return this.flushing;
|
|
89
|
+
if (this.buffer.length === 0) return;
|
|
90
|
+
const batch = this.buffer;
|
|
91
|
+
this.buffer = [];
|
|
92
|
+
this.flushing = this.send(batch).finally(() => {
|
|
93
|
+
this.flushing = null;
|
|
94
|
+
});
|
|
95
|
+
return this.flushing;
|
|
96
|
+
}
|
|
97
|
+
async close() {
|
|
98
|
+
this.closed = true;
|
|
99
|
+
if (this.timer) {
|
|
100
|
+
clearInterval(this.timer);
|
|
101
|
+
this.timer = null;
|
|
102
|
+
}
|
|
103
|
+
await this.flush();
|
|
104
|
+
}
|
|
105
|
+
async send(batch) {
|
|
106
|
+
try {
|
|
107
|
+
const res = await this.fetchImpl(this.endpoint, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
Authorization: `Bearer ${this.key}`
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify({ entries: batch }),
|
|
114
|
+
keepalive: true
|
|
115
|
+
});
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
throw new Error(`@clyse/trace: ingest failed with status ${res.status}`);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.onError(error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
mirrorToConsole(entry) {
|
|
124
|
+
const fn = entry.level === "error" ? console.error : entry.level === "warn" ? console.warn : entry.level === "debug" ? console.debug : console.info;
|
|
125
|
+
fn(`[trace:${entry.level}] ${entry.message}`, entry.meta ?? "");
|
|
126
|
+
}
|
|
127
|
+
registerExitHooks() {
|
|
128
|
+
const proc = globalThis.process;
|
|
129
|
+
if (proc?.on) {
|
|
130
|
+
proc.on("beforeExit", () => void this.flush());
|
|
131
|
+
proc.on("SIGTERM", () => void this.flush());
|
|
132
|
+
proc.on("SIGINT", () => void this.flush());
|
|
133
|
+
}
|
|
134
|
+
const w = globalThis;
|
|
135
|
+
if (typeof w.addEventListener === "function") {
|
|
136
|
+
w.addEventListener("beforeunload", () => void this.flush());
|
|
137
|
+
w.addEventListener("visibilitychange", () => {
|
|
138
|
+
if (w.document?.visibilityState === "hidden") void this.flush();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
function makeLogger(core, boundMeta) {
|
|
144
|
+
const log = (level, message, meta) => core.enqueue(level, message, mergeMeta(boundMeta, meta));
|
|
145
|
+
return {
|
|
146
|
+
debug: (m, meta) => log("debug", m, meta),
|
|
147
|
+
info: (m, meta) => log("info", m, meta),
|
|
148
|
+
warn: (m, meta) => log("warn", m, meta),
|
|
149
|
+
error: (m, meta) => log("error", m, meta),
|
|
150
|
+
log,
|
|
151
|
+
withTrace: (traceId) => makeLogger(core, mergeMeta(boundMeta, { trace_id: traceId })),
|
|
152
|
+
child: (meta) => makeLogger(core, mergeMeta(boundMeta, meta)),
|
|
153
|
+
flush: () => core.flush(),
|
|
154
|
+
close: () => core.close()
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function createTrace(options) {
|
|
158
|
+
return makeLogger(new TraceCore(options));
|
|
159
|
+
}
|
|
160
|
+
var index_default = createTrace;
|
|
161
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
162
|
+
0 && (module.exports = {
|
|
163
|
+
createTrace
|
|
164
|
+
});
|
|
165
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @clyse/trace โ tiny client for shipping logs to a Trace instance.\n *\n * Zero dependencies, works in Node 18+ and modern browsers (uses global fetch).\n * Logging never throws: transport failures are routed to `onError` and dropped.\n */\n\nexport type TraceLevel = 'debug' | 'info' | 'warn' | 'error';\nexport type Meta = Record<string, unknown>;\n\nexport interface TraceOptions {\n\t/** Base URL of the Trace API, e.g. \"https://trace.clyse.studio/api\" or \"http://localhost:4010\". */\n\turl: string;\n\t/** Application API key created in Trace settings (trc_โฆ). */\n\tkey: string;\n\t/** Flush the buffer at most every N ms. Default 2000. */\n\tflushInterval?: number;\n\t/** Flush as soon as the buffer reaches this many entries. Default 50. */\n\tbatchSize?: number;\n\t/** Hard cap on buffered entries; oldest are dropped beyond it. Default 1000. */\n\tmaxBuffer?: number;\n\t/** Meta merged into every entry (e.g. { env, service_version }). */\n\tdefaultMeta?: Meta;\n\t/** Also mirror entries to the local console. Default false. */\n\tconsole?: boolean;\n\t/** Called on transport errors (entries are dropped, never thrown). */\n\tonError?: (error: unknown) => void;\n\t/** Inject a custom fetch (tests, proxies). Defaults to global fetch. */\n\tfetch?: typeof fetch;\n\t/** Flush automatically on process exit / page unload. Default true. */\n\tflushOnExit?: boolean;\n}\n\nexport interface Trace {\n\tdebug(message: string, meta?: Meta): void;\n\tinfo(message: string, meta?: Meta): void;\n\twarn(message: string, meta?: Meta): void;\n\terror(message: string, meta?: Meta): void;\n\tlog(level: TraceLevel, message: string, meta?: Meta): void;\n\t/** Returns a logger that tags every entry with this trace id (correlation). */\n\twithTrace(traceId: string): Trace;\n\t/** Returns a logger with extra meta merged into every entry. */\n\tchild(meta: Meta): Trace;\n\t/** Send any buffered entries now. */\n\tflush(): Promise<void>;\n\t/** Flush and stop the background timer. */\n\tclose(): Promise<void>;\n}\n\ninterface Entry {\n\tlevel: TraceLevel;\n\tmessage: string;\n\tts: string;\n\tmeta?: Meta;\n}\n\nconst LEVELS: TraceLevel[] = ['debug', 'info', 'warn', 'error'];\n\nfunction normalizeBase(url: string): string {\n\tconst trimmed = url.replace(/\\/+$/, '');\n\treturn trimmed.endsWith('/ingest') ? trimmed : `${trimmed}/ingest`;\n}\n\nfunction mergeMeta(a?: Meta, b?: Meta): Meta | undefined {\n\tif (!a && !b) return undefined;\n\tconst out = { ...(a ?? {}), ...(b ?? {}) };\n\treturn Object.keys(out).length ? out : undefined;\n}\n\nclass TraceCore {\n\tprivate readonly endpoint: string;\n\tprivate readonly key: string;\n\tprivate readonly batchSize: number;\n\tprivate readonly maxBuffer: number;\n\tprivate readonly defaultMeta?: Meta;\n\tprivate readonly mirror: boolean;\n\tprivate readonly onError: (error: unknown) => void;\n\tprivate readonly fetchImpl: typeof fetch;\n\n\tprivate buffer: Entry[] = [];\n\tprivate timer: ReturnType<typeof setInterval> | null = null;\n\tprivate flushing: Promise<void> | null = null;\n\tprivate closed = false;\n\n\tconstructor(opts: TraceOptions) {\n\t\tif (!opts.url) throw new Error('@clyse/trace: `url` is required');\n\t\tif (!opts.key) throw new Error('@clyse/trace: `key` is required');\n\n\t\tthis.endpoint = normalizeBase(opts.url);\n\t\tthis.key = opts.key;\n\t\tthis.batchSize = opts.batchSize ?? 50;\n\t\tthis.maxBuffer = opts.maxBuffer ?? 1000;\n\t\tthis.defaultMeta = opts.defaultMeta;\n\t\tthis.mirror = opts.console ?? false;\n\t\tthis.onError = opts.onError ?? (() => {});\n\n\t\tconst f = opts.fetch ?? globalThis.fetch;\n\t\tif (typeof f !== 'function') {\n\t\t\tthrow new Error('@clyse/trace: no fetch available; pass options.fetch on older runtimes');\n\t\t}\n\t\tthis.fetchImpl = f.bind(globalThis);\n\n\t\tconst interval = opts.flushInterval ?? 2000;\n\t\tthis.timer = setInterval(() => void this.flush(), interval);\n\t\t// Don't keep a Node process alive just for the flush timer.\n\t\t(this.timer as { unref?: () => void }).unref?.();\n\n\t\tif (opts.flushOnExit ?? true) this.registerExitHooks();\n\t}\n\n\tenqueue(level: TraceLevel, message: string, meta?: Meta): void {\n\t\tif (this.closed) return;\n\t\tconst lvl = LEVELS.includes(level) ? level : 'info';\n\t\tconst entry: Entry = {\n\t\t\tlevel: lvl,\n\t\t\tmessage: typeof message === 'string' ? message : String(message),\n\t\t\tts: new Date().toISOString(),\n\t\t\tmeta: mergeMeta(this.defaultMeta, meta)\n\t\t};\n\n\t\tif (this.mirror) this.mirrorToConsole(entry);\n\n\t\tthis.buffer.push(entry);\n\t\tif (this.buffer.length > this.maxBuffer) {\n\t\t\tthis.buffer.splice(0, this.buffer.length - this.maxBuffer);\n\t\t}\n\t\tif (this.buffer.length >= this.batchSize) void this.flush();\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tif (this.flushing) return this.flushing;\n\t\tif (this.buffer.length === 0) return;\n\n\t\tconst batch = this.buffer;\n\t\tthis.buffer = [];\n\t\tthis.flushing = this.send(batch).finally(() => {\n\t\t\tthis.flushing = null;\n\t\t});\n\t\treturn this.flushing;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.closed = true;\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = null;\n\t\t}\n\t\tawait this.flush();\n\t}\n\n\tprivate async send(batch: Entry[]): Promise<void> {\n\t\ttry {\n\t\t\tconst res = await this.fetchImpl(this.endpoint, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\tAuthorization: `Bearer ${this.key}`\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({ entries: batch }),\n\t\t\t\tkeepalive: true\n\t\t\t});\n\t\t\tif (!res.ok) {\n\t\t\t\tthrow new Error(`@clyse/trace: ingest failed with status ${res.status}`);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.onError(error);\n\t\t}\n\t}\n\n\tprivate mirrorToConsole(entry: Entry): void {\n\t\tconst fn =\n\t\t\tentry.level === 'error'\n\t\t\t\t? console.error\n\t\t\t\t: entry.level === 'warn'\n\t\t\t\t\t? console.warn\n\t\t\t\t\t: entry.level === 'debug'\n\t\t\t\t\t\t? console.debug\n\t\t\t\t\t\t: console.info;\n\t\tfn(`[trace:${entry.level}] ${entry.message}`, entry.meta ?? '');\n\t}\n\n\tprivate registerExitHooks(): void {\n\t\tconst proc = (globalThis as { process?: { on?: (event: string, cb: () => void) => void } }).process;\n\t\tif (proc?.on) {\n\t\t\tproc.on('beforeExit', () => void this.flush());\n\t\t\tproc.on('SIGTERM', () => void this.flush());\n\t\t\tproc.on('SIGINT', () => void this.flush());\n\t\t}\n\t\tconst w = globalThis as typeof globalThis & {\n\t\t\taddEventListener?: (type: string, cb: () => void) => void;\n\t\t\tdocument?: { visibilityState?: string };\n\t\t};\n\t\tif (typeof w.addEventListener === 'function') {\n\t\t\tw.addEventListener('beforeunload', () => void this.flush());\n\t\t\tw.addEventListener('visibilitychange', () => {\n\t\t\t\tif (w.document?.visibilityState === 'hidden') void this.flush();\n\t\t\t});\n\t\t}\n\t}\n}\n\nfunction makeLogger(core: TraceCore, boundMeta?: Meta): Trace {\n\tconst log = (level: TraceLevel, message: string, meta?: Meta) =>\n\t\tcore.enqueue(level, message, mergeMeta(boundMeta, meta));\n\treturn {\n\t\tdebug: (m, meta) => log('debug', m, meta),\n\t\tinfo: (m, meta) => log('info', m, meta),\n\t\twarn: (m, meta) => log('warn', m, meta),\n\t\terror: (m, meta) => log('error', m, meta),\n\t\tlog,\n\t\twithTrace: (traceId) => makeLogger(core, mergeMeta(boundMeta, { trace_id: traceId })),\n\t\tchild: (meta) => makeLogger(core, mergeMeta(boundMeta, meta)),\n\t\tflush: () => core.flush(),\n\t\tclose: () => core.close()\n\t};\n}\n\n/** Create a Trace logger bound to an application key. */\nexport function createTrace(options: TraceOptions): Trace {\n\treturn makeLogger(new TraceCore(options));\n}\n\nexport default createTrace;\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clyse/trace โ tiny client for shipping logs to a Trace instance.
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies, works in Node 18+ and modern browsers (uses global fetch).
|
|
5
|
+
* Logging never throws: transport failures are routed to `onError` and dropped.
|
|
6
|
+
*/
|
|
7
|
+
type TraceLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
8
|
+
type Meta = Record<string, unknown>;
|
|
9
|
+
interface TraceOptions {
|
|
10
|
+
/** Base URL of the Trace API, e.g. "https://trace.clyse.studio/api" or "http://localhost:4010". */
|
|
11
|
+
url: string;
|
|
12
|
+
/** Application API key created in Trace settings (trc_โฆ). */
|
|
13
|
+
key: string;
|
|
14
|
+
/** Flush the buffer at most every N ms. Default 2000. */
|
|
15
|
+
flushInterval?: number;
|
|
16
|
+
/** Flush as soon as the buffer reaches this many entries. Default 50. */
|
|
17
|
+
batchSize?: number;
|
|
18
|
+
/** Hard cap on buffered entries; oldest are dropped beyond it. Default 1000. */
|
|
19
|
+
maxBuffer?: number;
|
|
20
|
+
/** Meta merged into every entry (e.g. { env, service_version }). */
|
|
21
|
+
defaultMeta?: Meta;
|
|
22
|
+
/** Also mirror entries to the local console. Default false. */
|
|
23
|
+
console?: boolean;
|
|
24
|
+
/** Called on transport errors (entries are dropped, never thrown). */
|
|
25
|
+
onError?: (error: unknown) => void;
|
|
26
|
+
/** Inject a custom fetch (tests, proxies). Defaults to global fetch. */
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
/** Flush automatically on process exit / page unload. Default true. */
|
|
29
|
+
flushOnExit?: boolean;
|
|
30
|
+
}
|
|
31
|
+
interface Trace {
|
|
32
|
+
debug(message: string, meta?: Meta): void;
|
|
33
|
+
info(message: string, meta?: Meta): void;
|
|
34
|
+
warn(message: string, meta?: Meta): void;
|
|
35
|
+
error(message: string, meta?: Meta): void;
|
|
36
|
+
log(level: TraceLevel, message: string, meta?: Meta): void;
|
|
37
|
+
/** Returns a logger that tags every entry with this trace id (correlation). */
|
|
38
|
+
withTrace(traceId: string): Trace;
|
|
39
|
+
/** Returns a logger with extra meta merged into every entry. */
|
|
40
|
+
child(meta: Meta): Trace;
|
|
41
|
+
/** Send any buffered entries now. */
|
|
42
|
+
flush(): Promise<void>;
|
|
43
|
+
/** Flush and stop the background timer. */
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/** Create a Trace logger bound to an application key. */
|
|
47
|
+
declare function createTrace(options: TraceOptions): Trace;
|
|
48
|
+
|
|
49
|
+
export { type Meta, type Trace, type TraceLevel, type TraceOptions, createTrace, createTrace as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clyse/trace โ tiny client for shipping logs to a Trace instance.
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies, works in Node 18+ and modern browsers (uses global fetch).
|
|
5
|
+
* Logging never throws: transport failures are routed to `onError` and dropped.
|
|
6
|
+
*/
|
|
7
|
+
type TraceLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
8
|
+
type Meta = Record<string, unknown>;
|
|
9
|
+
interface TraceOptions {
|
|
10
|
+
/** Base URL of the Trace API, e.g. "https://trace.clyse.studio/api" or "http://localhost:4010". */
|
|
11
|
+
url: string;
|
|
12
|
+
/** Application API key created in Trace settings (trc_โฆ). */
|
|
13
|
+
key: string;
|
|
14
|
+
/** Flush the buffer at most every N ms. Default 2000. */
|
|
15
|
+
flushInterval?: number;
|
|
16
|
+
/** Flush as soon as the buffer reaches this many entries. Default 50. */
|
|
17
|
+
batchSize?: number;
|
|
18
|
+
/** Hard cap on buffered entries; oldest are dropped beyond it. Default 1000. */
|
|
19
|
+
maxBuffer?: number;
|
|
20
|
+
/** Meta merged into every entry (e.g. { env, service_version }). */
|
|
21
|
+
defaultMeta?: Meta;
|
|
22
|
+
/** Also mirror entries to the local console. Default false. */
|
|
23
|
+
console?: boolean;
|
|
24
|
+
/** Called on transport errors (entries are dropped, never thrown). */
|
|
25
|
+
onError?: (error: unknown) => void;
|
|
26
|
+
/** Inject a custom fetch (tests, proxies). Defaults to global fetch. */
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
/** Flush automatically on process exit / page unload. Default true. */
|
|
29
|
+
flushOnExit?: boolean;
|
|
30
|
+
}
|
|
31
|
+
interface Trace {
|
|
32
|
+
debug(message: string, meta?: Meta): void;
|
|
33
|
+
info(message: string, meta?: Meta): void;
|
|
34
|
+
warn(message: string, meta?: Meta): void;
|
|
35
|
+
error(message: string, meta?: Meta): void;
|
|
36
|
+
log(level: TraceLevel, message: string, meta?: Meta): void;
|
|
37
|
+
/** Returns a logger that tags every entry with this trace id (correlation). */
|
|
38
|
+
withTrace(traceId: string): Trace;
|
|
39
|
+
/** Returns a logger with extra meta merged into every entry. */
|
|
40
|
+
child(meta: Meta): Trace;
|
|
41
|
+
/** Send any buffered entries now. */
|
|
42
|
+
flush(): Promise<void>;
|
|
43
|
+
/** Flush and stop the background timer. */
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/** Create a Trace logger bound to an application key. */
|
|
47
|
+
declare function createTrace(options: TraceOptions): Trace;
|
|
48
|
+
|
|
49
|
+
export { type Meta, type Trace, type TraceLevel, type TraceOptions, createTrace, createTrace as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var LEVELS = ["debug", "info", "warn", "error"];
|
|
3
|
+
function normalizeBase(url) {
|
|
4
|
+
const trimmed = url.replace(/\/+$/, "");
|
|
5
|
+
return trimmed.endsWith("/ingest") ? trimmed : `${trimmed}/ingest`;
|
|
6
|
+
}
|
|
7
|
+
function mergeMeta(a, b) {
|
|
8
|
+
if (!a && !b) return void 0;
|
|
9
|
+
const out = { ...a ?? {}, ...b ?? {} };
|
|
10
|
+
return Object.keys(out).length ? out : void 0;
|
|
11
|
+
}
|
|
12
|
+
var TraceCore = class {
|
|
13
|
+
endpoint;
|
|
14
|
+
key;
|
|
15
|
+
batchSize;
|
|
16
|
+
maxBuffer;
|
|
17
|
+
defaultMeta;
|
|
18
|
+
mirror;
|
|
19
|
+
onError;
|
|
20
|
+
fetchImpl;
|
|
21
|
+
buffer = [];
|
|
22
|
+
timer = null;
|
|
23
|
+
flushing = null;
|
|
24
|
+
closed = false;
|
|
25
|
+
constructor(opts) {
|
|
26
|
+
if (!opts.url) throw new Error("@clyse/trace: `url` is required");
|
|
27
|
+
if (!opts.key) throw new Error("@clyse/trace: `key` is required");
|
|
28
|
+
this.endpoint = normalizeBase(opts.url);
|
|
29
|
+
this.key = opts.key;
|
|
30
|
+
this.batchSize = opts.batchSize ?? 50;
|
|
31
|
+
this.maxBuffer = opts.maxBuffer ?? 1e3;
|
|
32
|
+
this.defaultMeta = opts.defaultMeta;
|
|
33
|
+
this.mirror = opts.console ?? false;
|
|
34
|
+
this.onError = opts.onError ?? (() => {
|
|
35
|
+
});
|
|
36
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
37
|
+
if (typeof f !== "function") {
|
|
38
|
+
throw new Error("@clyse/trace: no fetch available; pass options.fetch on older runtimes");
|
|
39
|
+
}
|
|
40
|
+
this.fetchImpl = f.bind(globalThis);
|
|
41
|
+
const interval = opts.flushInterval ?? 2e3;
|
|
42
|
+
this.timer = setInterval(() => void this.flush(), interval);
|
|
43
|
+
this.timer.unref?.();
|
|
44
|
+
if (opts.flushOnExit ?? true) this.registerExitHooks();
|
|
45
|
+
}
|
|
46
|
+
enqueue(level, message, meta) {
|
|
47
|
+
if (this.closed) return;
|
|
48
|
+
const lvl = LEVELS.includes(level) ? level : "info";
|
|
49
|
+
const entry = {
|
|
50
|
+
level: lvl,
|
|
51
|
+
message: typeof message === "string" ? message : String(message),
|
|
52
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
53
|
+
meta: mergeMeta(this.defaultMeta, meta)
|
|
54
|
+
};
|
|
55
|
+
if (this.mirror) this.mirrorToConsole(entry);
|
|
56
|
+
this.buffer.push(entry);
|
|
57
|
+
if (this.buffer.length > this.maxBuffer) {
|
|
58
|
+
this.buffer.splice(0, this.buffer.length - this.maxBuffer);
|
|
59
|
+
}
|
|
60
|
+
if (this.buffer.length >= this.batchSize) void this.flush();
|
|
61
|
+
}
|
|
62
|
+
async flush() {
|
|
63
|
+
if (this.flushing) return this.flushing;
|
|
64
|
+
if (this.buffer.length === 0) return;
|
|
65
|
+
const batch = this.buffer;
|
|
66
|
+
this.buffer = [];
|
|
67
|
+
this.flushing = this.send(batch).finally(() => {
|
|
68
|
+
this.flushing = null;
|
|
69
|
+
});
|
|
70
|
+
return this.flushing;
|
|
71
|
+
}
|
|
72
|
+
async close() {
|
|
73
|
+
this.closed = true;
|
|
74
|
+
if (this.timer) {
|
|
75
|
+
clearInterval(this.timer);
|
|
76
|
+
this.timer = null;
|
|
77
|
+
}
|
|
78
|
+
await this.flush();
|
|
79
|
+
}
|
|
80
|
+
async send(batch) {
|
|
81
|
+
try {
|
|
82
|
+
const res = await this.fetchImpl(this.endpoint, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
Authorization: `Bearer ${this.key}`
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify({ entries: batch }),
|
|
89
|
+
keepalive: true
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
throw new Error(`@clyse/trace: ingest failed with status ${res.status}`);
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
this.onError(error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
mirrorToConsole(entry) {
|
|
99
|
+
const fn = entry.level === "error" ? console.error : entry.level === "warn" ? console.warn : entry.level === "debug" ? console.debug : console.info;
|
|
100
|
+
fn(`[trace:${entry.level}] ${entry.message}`, entry.meta ?? "");
|
|
101
|
+
}
|
|
102
|
+
registerExitHooks() {
|
|
103
|
+
const proc = globalThis.process;
|
|
104
|
+
if (proc?.on) {
|
|
105
|
+
proc.on("beforeExit", () => void this.flush());
|
|
106
|
+
proc.on("SIGTERM", () => void this.flush());
|
|
107
|
+
proc.on("SIGINT", () => void this.flush());
|
|
108
|
+
}
|
|
109
|
+
const w = globalThis;
|
|
110
|
+
if (typeof w.addEventListener === "function") {
|
|
111
|
+
w.addEventListener("beforeunload", () => void this.flush());
|
|
112
|
+
w.addEventListener("visibilitychange", () => {
|
|
113
|
+
if (w.document?.visibilityState === "hidden") void this.flush();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function makeLogger(core, boundMeta) {
|
|
119
|
+
const log = (level, message, meta) => core.enqueue(level, message, mergeMeta(boundMeta, meta));
|
|
120
|
+
return {
|
|
121
|
+
debug: (m, meta) => log("debug", m, meta),
|
|
122
|
+
info: (m, meta) => log("info", m, meta),
|
|
123
|
+
warn: (m, meta) => log("warn", m, meta),
|
|
124
|
+
error: (m, meta) => log("error", m, meta),
|
|
125
|
+
log,
|
|
126
|
+
withTrace: (traceId) => makeLogger(core, mergeMeta(boundMeta, { trace_id: traceId })),
|
|
127
|
+
child: (meta) => makeLogger(core, mergeMeta(boundMeta, meta)),
|
|
128
|
+
flush: () => core.flush(),
|
|
129
|
+
close: () => core.close()
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function createTrace(options) {
|
|
133
|
+
return makeLogger(new TraceCore(options));
|
|
134
|
+
}
|
|
135
|
+
var index_default = createTrace;
|
|
136
|
+
export {
|
|
137
|
+
createTrace,
|
|
138
|
+
index_default as default
|
|
139
|
+
};
|
|
140
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @clyse/trace โ tiny client for shipping logs to a Trace instance.\n *\n * Zero dependencies, works in Node 18+ and modern browsers (uses global fetch).\n * Logging never throws: transport failures are routed to `onError` and dropped.\n */\n\nexport type TraceLevel = 'debug' | 'info' | 'warn' | 'error';\nexport type Meta = Record<string, unknown>;\n\nexport interface TraceOptions {\n\t/** Base URL of the Trace API, e.g. \"https://trace.clyse.studio/api\" or \"http://localhost:4010\". */\n\turl: string;\n\t/** Application API key created in Trace settings (trc_โฆ). */\n\tkey: string;\n\t/** Flush the buffer at most every N ms. Default 2000. */\n\tflushInterval?: number;\n\t/** Flush as soon as the buffer reaches this many entries. Default 50. */\n\tbatchSize?: number;\n\t/** Hard cap on buffered entries; oldest are dropped beyond it. Default 1000. */\n\tmaxBuffer?: number;\n\t/** Meta merged into every entry (e.g. { env, service_version }). */\n\tdefaultMeta?: Meta;\n\t/** Also mirror entries to the local console. Default false. */\n\tconsole?: boolean;\n\t/** Called on transport errors (entries are dropped, never thrown). */\n\tonError?: (error: unknown) => void;\n\t/** Inject a custom fetch (tests, proxies). Defaults to global fetch. */\n\tfetch?: typeof fetch;\n\t/** Flush automatically on process exit / page unload. Default true. */\n\tflushOnExit?: boolean;\n}\n\nexport interface Trace {\n\tdebug(message: string, meta?: Meta): void;\n\tinfo(message: string, meta?: Meta): void;\n\twarn(message: string, meta?: Meta): void;\n\terror(message: string, meta?: Meta): void;\n\tlog(level: TraceLevel, message: string, meta?: Meta): void;\n\t/** Returns a logger that tags every entry with this trace id (correlation). */\n\twithTrace(traceId: string): Trace;\n\t/** Returns a logger with extra meta merged into every entry. */\n\tchild(meta: Meta): Trace;\n\t/** Send any buffered entries now. */\n\tflush(): Promise<void>;\n\t/** Flush and stop the background timer. */\n\tclose(): Promise<void>;\n}\n\ninterface Entry {\n\tlevel: TraceLevel;\n\tmessage: string;\n\tts: string;\n\tmeta?: Meta;\n}\n\nconst LEVELS: TraceLevel[] = ['debug', 'info', 'warn', 'error'];\n\nfunction normalizeBase(url: string): string {\n\tconst trimmed = url.replace(/\\/+$/, '');\n\treturn trimmed.endsWith('/ingest') ? trimmed : `${trimmed}/ingest`;\n}\n\nfunction mergeMeta(a?: Meta, b?: Meta): Meta | undefined {\n\tif (!a && !b) return undefined;\n\tconst out = { ...(a ?? {}), ...(b ?? {}) };\n\treturn Object.keys(out).length ? out : undefined;\n}\n\nclass TraceCore {\n\tprivate readonly endpoint: string;\n\tprivate readonly key: string;\n\tprivate readonly batchSize: number;\n\tprivate readonly maxBuffer: number;\n\tprivate readonly defaultMeta?: Meta;\n\tprivate readonly mirror: boolean;\n\tprivate readonly onError: (error: unknown) => void;\n\tprivate readonly fetchImpl: typeof fetch;\n\n\tprivate buffer: Entry[] = [];\n\tprivate timer: ReturnType<typeof setInterval> | null = null;\n\tprivate flushing: Promise<void> | null = null;\n\tprivate closed = false;\n\n\tconstructor(opts: TraceOptions) {\n\t\tif (!opts.url) throw new Error('@clyse/trace: `url` is required');\n\t\tif (!opts.key) throw new Error('@clyse/trace: `key` is required');\n\n\t\tthis.endpoint = normalizeBase(opts.url);\n\t\tthis.key = opts.key;\n\t\tthis.batchSize = opts.batchSize ?? 50;\n\t\tthis.maxBuffer = opts.maxBuffer ?? 1000;\n\t\tthis.defaultMeta = opts.defaultMeta;\n\t\tthis.mirror = opts.console ?? false;\n\t\tthis.onError = opts.onError ?? (() => {});\n\n\t\tconst f = opts.fetch ?? globalThis.fetch;\n\t\tif (typeof f !== 'function') {\n\t\t\tthrow new Error('@clyse/trace: no fetch available; pass options.fetch on older runtimes');\n\t\t}\n\t\tthis.fetchImpl = f.bind(globalThis);\n\n\t\tconst interval = opts.flushInterval ?? 2000;\n\t\tthis.timer = setInterval(() => void this.flush(), interval);\n\t\t// Don't keep a Node process alive just for the flush timer.\n\t\t(this.timer as { unref?: () => void }).unref?.();\n\n\t\tif (opts.flushOnExit ?? true) this.registerExitHooks();\n\t}\n\n\tenqueue(level: TraceLevel, message: string, meta?: Meta): void {\n\t\tif (this.closed) return;\n\t\tconst lvl = LEVELS.includes(level) ? level : 'info';\n\t\tconst entry: Entry = {\n\t\t\tlevel: lvl,\n\t\t\tmessage: typeof message === 'string' ? message : String(message),\n\t\t\tts: new Date().toISOString(),\n\t\t\tmeta: mergeMeta(this.defaultMeta, meta)\n\t\t};\n\n\t\tif (this.mirror) this.mirrorToConsole(entry);\n\n\t\tthis.buffer.push(entry);\n\t\tif (this.buffer.length > this.maxBuffer) {\n\t\t\tthis.buffer.splice(0, this.buffer.length - this.maxBuffer);\n\t\t}\n\t\tif (this.buffer.length >= this.batchSize) void this.flush();\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tif (this.flushing) return this.flushing;\n\t\tif (this.buffer.length === 0) return;\n\n\t\tconst batch = this.buffer;\n\t\tthis.buffer = [];\n\t\tthis.flushing = this.send(batch).finally(() => {\n\t\t\tthis.flushing = null;\n\t\t});\n\t\treturn this.flushing;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.closed = true;\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = null;\n\t\t}\n\t\tawait this.flush();\n\t}\n\n\tprivate async send(batch: Entry[]): Promise<void> {\n\t\ttry {\n\t\t\tconst res = await this.fetchImpl(this.endpoint, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\tAuthorization: `Bearer ${this.key}`\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({ entries: batch }),\n\t\t\t\tkeepalive: true\n\t\t\t});\n\t\t\tif (!res.ok) {\n\t\t\t\tthrow new Error(`@clyse/trace: ingest failed with status ${res.status}`);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.onError(error);\n\t\t}\n\t}\n\n\tprivate mirrorToConsole(entry: Entry): void {\n\t\tconst fn =\n\t\t\tentry.level === 'error'\n\t\t\t\t? console.error\n\t\t\t\t: entry.level === 'warn'\n\t\t\t\t\t? console.warn\n\t\t\t\t\t: entry.level === 'debug'\n\t\t\t\t\t\t? console.debug\n\t\t\t\t\t\t: console.info;\n\t\tfn(`[trace:${entry.level}] ${entry.message}`, entry.meta ?? '');\n\t}\n\n\tprivate registerExitHooks(): void {\n\t\tconst proc = (globalThis as { process?: { on?: (event: string, cb: () => void) => void } }).process;\n\t\tif (proc?.on) {\n\t\t\tproc.on('beforeExit', () => void this.flush());\n\t\t\tproc.on('SIGTERM', () => void this.flush());\n\t\t\tproc.on('SIGINT', () => void this.flush());\n\t\t}\n\t\tconst w = globalThis as typeof globalThis & {\n\t\t\taddEventListener?: (type: string, cb: () => void) => void;\n\t\t\tdocument?: { visibilityState?: string };\n\t\t};\n\t\tif (typeof w.addEventListener === 'function') {\n\t\t\tw.addEventListener('beforeunload', () => void this.flush());\n\t\t\tw.addEventListener('visibilitychange', () => {\n\t\t\t\tif (w.document?.visibilityState === 'hidden') void this.flush();\n\t\t\t});\n\t\t}\n\t}\n}\n\nfunction makeLogger(core: TraceCore, boundMeta?: Meta): Trace {\n\tconst log = (level: TraceLevel, message: string, meta?: Meta) =>\n\t\tcore.enqueue(level, message, mergeMeta(boundMeta, meta));\n\treturn {\n\t\tdebug: (m, meta) => log('debug', m, meta),\n\t\tinfo: (m, meta) => log('info', m, meta),\n\t\twarn: (m, meta) => log('warn', m, meta),\n\t\terror: (m, meta) => log('error', m, meta),\n\t\tlog,\n\t\twithTrace: (traceId) => makeLogger(core, mergeMeta(boundMeta, { trace_id: traceId })),\n\t\tchild: (meta) => makeLogger(core, mergeMeta(boundMeta, meta)),\n\t\tflush: () => core.flush(),\n\t\tclose: () => core.close()\n\t};\n}\n\n/** Create a Trace logger bound to an application key. */\nexport function createTrace(options: TraceOptions): Trace {\n\treturn makeLogger(new TraceCore(options));\n}\n\nexport default createTrace;\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clyse/trace",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tiny zero-dependency client for shipping logs to a Trace instance.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
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
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"test": "bun test",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"prepublishOnly": "bun run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"trace",
|
|
28
|
+
"logging",
|
|
29
|
+
"logs",
|
|
30
|
+
"observability",
|
|
31
|
+
"clyse"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/Clyse-Devs/Trace.git",
|
|
37
|
+
"directory": "packages/trace"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"tsup": "^8.5.0",
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
|
+
}
|
|
49
|
+
}
|