@camstack/system 1.2.17 → 1.2.19
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/dist/builtins/loki-logging/index.js +56 -15
- package/dist/builtins/loki-logging/index.mjs +56 -15
- package/dist/builtins/loki-logging/loki-destination.d.ts +11 -0
- package/dist/builtins/loki-logging/loki-logging.addon.d.ts +8 -0
- package/dist/builtins/loki-logging/loki-payload.d.ts +12 -1
- package/package.json +1 -1
|
@@ -50,6 +50,12 @@ function formatLokiLine(entry) {
|
|
|
50
50
|
* Group a batch into one stream per distinct label set, and sort each stream's
|
|
51
51
|
* values by timestamp.
|
|
52
52
|
*
|
|
53
|
+
* The `node` label is resolved PER ENTRY from `tags.agentId` (the emitting
|
|
54
|
+
* agent, falling back to `labels.node` when absent), so a batch that mixes
|
|
55
|
+
* hub-local lines with forwarded agent lines fans out into one stream per
|
|
56
|
+
* origin node — the whole point of shipping the cluster's logs through the hub
|
|
57
|
+
* without flattening every line onto `node="hub"`.
|
|
58
|
+
*
|
|
53
59
|
* The sort is not cosmetic. Loki historically rejected a stream whose entries
|
|
54
60
|
* were not in ascending time order, and even with out-of-order ingestion enabled
|
|
55
61
|
* it is bounded by a window. Entries reach a destination from several concurrent
|
|
@@ -58,21 +64,26 @@ function formatLokiLine(entry) {
|
|
|
58
64
|
*/
|
|
59
65
|
function buildPushBody(entries, labels) {
|
|
60
66
|
if (entries.length === 0) return null;
|
|
61
|
-
const base = {
|
|
62
|
-
job: labels.job,
|
|
63
|
-
node: labels.node
|
|
64
|
-
};
|
|
67
|
+
const base = { job: labels.job };
|
|
65
68
|
for (const [key, value] of Object.entries(labels.extra ?? {})) base[key] = value;
|
|
66
|
-
const
|
|
69
|
+
const byStream = /* @__PURE__ */ new Map();
|
|
67
70
|
for (const entry of entries) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
const agentId = entry.tags?.agentId;
|
|
72
|
+
const node = typeof agentId === "string" && agentId.length > 0 ? agentId : labels.node;
|
|
73
|
+
const key = `${node}\u0000${entry.level}`;
|
|
74
|
+
const bucket = byStream.get(key) ?? {
|
|
75
|
+
node,
|
|
76
|
+
level: entry.level,
|
|
77
|
+
values: []
|
|
78
|
+
};
|
|
79
|
+
bucket.values.push([toUnixNano(entry.timestamp), formatLokiLine(entry)]);
|
|
80
|
+
byStream.set(key, bucket);
|
|
71
81
|
}
|
|
72
82
|
const streams = [];
|
|
73
|
-
for (const
|
|
83
|
+
for (const { node, level, values } of byStream.values()) streams.push({
|
|
74
84
|
stream: {
|
|
75
85
|
...base,
|
|
86
|
+
node,
|
|
76
87
|
level
|
|
77
88
|
},
|
|
78
89
|
values: values.toSorted((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
|
|
@@ -118,10 +129,32 @@ var LokiDestination = class {
|
|
|
118
129
|
}
|
|
119
130
|
async initialize(config) {
|
|
120
131
|
this.config = config ?? null;
|
|
121
|
-
|
|
132
|
+
this.startTimerIfActive();
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Re-apply configuration to an already-initialized destination after a
|
|
136
|
+
* settings change, without dropping the queued tail. Stops the current flush
|
|
137
|
+
* timer, swaps the config in, and starts a fresh timer only when the new
|
|
138
|
+
* config is active. This is what makes the addon's "enable without a restart"
|
|
139
|
+
* contract real: `initialize()` starts a timer, so calling it twice would leak
|
|
140
|
+
* the first one — `reconfigure()` clears it first.
|
|
141
|
+
*/
|
|
142
|
+
async reconfigure(config) {
|
|
143
|
+
this.stopTimer();
|
|
144
|
+
this.config = config;
|
|
145
|
+
this.startTimerIfActive();
|
|
146
|
+
}
|
|
147
|
+
stopTimer() {
|
|
148
|
+
if (this.timer !== void 0) {
|
|
149
|
+
this.clearIntervalFn(this.timer);
|
|
150
|
+
this.timer = void 0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
startTimerIfActive() {
|
|
154
|
+
if (!this.isActive() || this.config === null) return;
|
|
122
155
|
this.timer = this.setIntervalFn(() => {
|
|
123
156
|
this.flush();
|
|
124
|
-
}, Math.max(250, config
|
|
157
|
+
}, Math.max(250, this.config.batchIntervalMs));
|
|
125
158
|
const timer = this.timer;
|
|
126
159
|
if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
|
|
127
160
|
}
|
|
@@ -254,10 +287,7 @@ var LokiDestination = class {
|
|
|
254
287
|
return [];
|
|
255
288
|
}
|
|
256
289
|
async shutdown() {
|
|
257
|
-
|
|
258
|
-
this.clearIntervalFn(this.timer);
|
|
259
|
-
this.timer = void 0;
|
|
260
|
-
}
|
|
290
|
+
this.stopTimer();
|
|
261
291
|
await this.flush();
|
|
262
292
|
this.config = null;
|
|
263
293
|
}
|
|
@@ -296,6 +326,17 @@ var LokiLoggingAddon = class extends require_dist.BaseAddon {
|
|
|
296
326
|
this.destination = null;
|
|
297
327
|
}
|
|
298
328
|
/**
|
|
329
|
+
* React to a settings change (enable/disable, URL, level, batching) without a
|
|
330
|
+
* restart — the contract this addon advertises. `BaseAddon` has already
|
|
331
|
+
* reloaded `this.config` by the time this runs; re-apply it to the live
|
|
332
|
+
* destination so toggling `enabled` on actually starts pushing (and off stops
|
|
333
|
+
* it). Without this the destination keeps its boot-time config forever.
|
|
334
|
+
*/
|
|
335
|
+
async onConfigChanged() {
|
|
336
|
+
await this.destination?.reconfigure(this.destinationConfig());
|
|
337
|
+
this.ctx.logger.info(this.config.enabled && this.config.url.length > 0 ? `Loki logging reconfigured → ${this.config.url}` : "Loki logging now inactive (disabled or no URL)");
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
299
340
|
* `node` is a Loki LABEL, so it must be the node's real identity and must not
|
|
300
341
|
* be operator-editable — a typo would fork the stream and split a node's history
|
|
301
342
|
* in two. `kernel.localNodeId` is the Moleculer node id (`'hub'` on the hub, the
|
|
@@ -45,6 +45,12 @@ function formatLokiLine(entry) {
|
|
|
45
45
|
* Group a batch into one stream per distinct label set, and sort each stream's
|
|
46
46
|
* values by timestamp.
|
|
47
47
|
*
|
|
48
|
+
* The `node` label is resolved PER ENTRY from `tags.agentId` (the emitting
|
|
49
|
+
* agent, falling back to `labels.node` when absent), so a batch that mixes
|
|
50
|
+
* hub-local lines with forwarded agent lines fans out into one stream per
|
|
51
|
+
* origin node — the whole point of shipping the cluster's logs through the hub
|
|
52
|
+
* without flattening every line onto `node="hub"`.
|
|
53
|
+
*
|
|
48
54
|
* The sort is not cosmetic. Loki historically rejected a stream whose entries
|
|
49
55
|
* were not in ascending time order, and even with out-of-order ingestion enabled
|
|
50
56
|
* it is bounded by a window. Entries reach a destination from several concurrent
|
|
@@ -53,21 +59,26 @@ function formatLokiLine(entry) {
|
|
|
53
59
|
*/
|
|
54
60
|
function buildPushBody(entries, labels) {
|
|
55
61
|
if (entries.length === 0) return null;
|
|
56
|
-
const base = {
|
|
57
|
-
job: labels.job,
|
|
58
|
-
node: labels.node
|
|
59
|
-
};
|
|
62
|
+
const base = { job: labels.job };
|
|
60
63
|
for (const [key, value] of Object.entries(labels.extra ?? {})) base[key] = value;
|
|
61
|
-
const
|
|
64
|
+
const byStream = /* @__PURE__ */ new Map();
|
|
62
65
|
for (const entry of entries) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
const agentId = entry.tags?.agentId;
|
|
67
|
+
const node = typeof agentId === "string" && agentId.length > 0 ? agentId : labels.node;
|
|
68
|
+
const key = `${node}\u0000${entry.level}`;
|
|
69
|
+
const bucket = byStream.get(key) ?? {
|
|
70
|
+
node,
|
|
71
|
+
level: entry.level,
|
|
72
|
+
values: []
|
|
73
|
+
};
|
|
74
|
+
bucket.values.push([toUnixNano(entry.timestamp), formatLokiLine(entry)]);
|
|
75
|
+
byStream.set(key, bucket);
|
|
66
76
|
}
|
|
67
77
|
const streams = [];
|
|
68
|
-
for (const
|
|
78
|
+
for (const { node, level, values } of byStream.values()) streams.push({
|
|
69
79
|
stream: {
|
|
70
80
|
...base,
|
|
81
|
+
node,
|
|
71
82
|
level
|
|
72
83
|
},
|
|
73
84
|
values: values.toSorted((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
|
|
@@ -113,10 +124,32 @@ var LokiDestination = class {
|
|
|
113
124
|
}
|
|
114
125
|
async initialize(config) {
|
|
115
126
|
this.config = config ?? null;
|
|
116
|
-
|
|
127
|
+
this.startTimerIfActive();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Re-apply configuration to an already-initialized destination after a
|
|
131
|
+
* settings change, without dropping the queued tail. Stops the current flush
|
|
132
|
+
* timer, swaps the config in, and starts a fresh timer only when the new
|
|
133
|
+
* config is active. This is what makes the addon's "enable without a restart"
|
|
134
|
+
* contract real: `initialize()` starts a timer, so calling it twice would leak
|
|
135
|
+
* the first one — `reconfigure()` clears it first.
|
|
136
|
+
*/
|
|
137
|
+
async reconfigure(config) {
|
|
138
|
+
this.stopTimer();
|
|
139
|
+
this.config = config;
|
|
140
|
+
this.startTimerIfActive();
|
|
141
|
+
}
|
|
142
|
+
stopTimer() {
|
|
143
|
+
if (this.timer !== void 0) {
|
|
144
|
+
this.clearIntervalFn(this.timer);
|
|
145
|
+
this.timer = void 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
startTimerIfActive() {
|
|
149
|
+
if (!this.isActive() || this.config === null) return;
|
|
117
150
|
this.timer = this.setIntervalFn(() => {
|
|
118
151
|
this.flush();
|
|
119
|
-
}, Math.max(250, config
|
|
152
|
+
}, Math.max(250, this.config.batchIntervalMs));
|
|
120
153
|
const timer = this.timer;
|
|
121
154
|
if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
|
|
122
155
|
}
|
|
@@ -249,10 +282,7 @@ var LokiDestination = class {
|
|
|
249
282
|
return [];
|
|
250
283
|
}
|
|
251
284
|
async shutdown() {
|
|
252
|
-
|
|
253
|
-
this.clearIntervalFn(this.timer);
|
|
254
|
-
this.timer = void 0;
|
|
255
|
-
}
|
|
285
|
+
this.stopTimer();
|
|
256
286
|
await this.flush();
|
|
257
287
|
this.config = null;
|
|
258
288
|
}
|
|
@@ -291,6 +321,17 @@ var LokiLoggingAddon = class extends BaseAddon {
|
|
|
291
321
|
this.destination = null;
|
|
292
322
|
}
|
|
293
323
|
/**
|
|
324
|
+
* React to a settings change (enable/disable, URL, level, batching) without a
|
|
325
|
+
* restart — the contract this addon advertises. `BaseAddon` has already
|
|
326
|
+
* reloaded `this.config` by the time this runs; re-apply it to the live
|
|
327
|
+
* destination so toggling `enabled` on actually starts pushing (and off stops
|
|
328
|
+
* it). Without this the destination keeps its boot-time config forever.
|
|
329
|
+
*/
|
|
330
|
+
async onConfigChanged() {
|
|
331
|
+
await this.destination?.reconfigure(this.destinationConfig());
|
|
332
|
+
this.ctx.logger.info(this.config.enabled && this.config.url.length > 0 ? `Loki logging reconfigured → ${this.config.url}` : "Loki logging now inactive (disabled or no URL)");
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
294
335
|
* `node` is a Loki LABEL, so it must be the node's real identity and must not
|
|
295
336
|
* be operator-editable — a typo would fork the stream and split a node's history
|
|
296
337
|
* in two. `kernel.localNodeId` is the Moleculer node id (`'hub'` on the hub, the
|
|
@@ -60,6 +60,17 @@ export declare class LokiDestination implements ILogDestination {
|
|
|
60
60
|
private readonly clearIntervalFn;
|
|
61
61
|
constructor(deps?: LokiDestinationDeps);
|
|
62
62
|
initialize(config?: LokiConfig): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Re-apply configuration to an already-initialized destination after a
|
|
65
|
+
* settings change, without dropping the queued tail. Stops the current flush
|
|
66
|
+
* timer, swaps the config in, and starts a fresh timer only when the new
|
|
67
|
+
* config is active. This is what makes the addon's "enable without a restart"
|
|
68
|
+
* contract real: `initialize()` starts a timer, so calling it twice would leak
|
|
69
|
+
* the first one — `reconfigure()` clears it first.
|
|
70
|
+
*/
|
|
71
|
+
reconfigure(config: LokiConfig): Promise<void>;
|
|
72
|
+
private stopTimer;
|
|
73
|
+
private startTimerIfActive;
|
|
63
74
|
/** Enabled AND pointed somewhere. An enabled destination with no URL is inert. */
|
|
64
75
|
private isActive;
|
|
65
76
|
write(entry: LogEntry): void;
|
|
@@ -18,6 +18,14 @@ export declare class LokiLoggingAddon extends BaseAddon<LokiAddonConfig> {
|
|
|
18
18
|
constructor();
|
|
19
19
|
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
20
20
|
protected onShutdown(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* React to a settings change (enable/disable, URL, level, batching) without a
|
|
23
|
+
* restart — the contract this addon advertises. `BaseAddon` has already
|
|
24
|
+
* reloaded `this.config` by the time this runs; re-apply it to the live
|
|
25
|
+
* destination so toggling `enabled` on actually starts pushing (and off stops
|
|
26
|
+
* it). Without this the destination keeps its boot-time config forever.
|
|
27
|
+
*/
|
|
28
|
+
protected onConfigChanged(): Promise<void>;
|
|
21
29
|
/**
|
|
22
30
|
* `node` is a Loki LABEL, so it must be the node's real identity and must not
|
|
23
31
|
* be operator-editable — a typo would fork the stream and split a node's history
|
|
@@ -11,7 +11,12 @@ export interface LokiPushBody {
|
|
|
11
11
|
export interface LokiLabelInput {
|
|
12
12
|
/** Fixed `job` label. Bounded by configuration, never per-entry. */
|
|
13
13
|
readonly job: string;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Fallback node id for entries that carry no `tags.agentId` origin — the
|
|
16
|
+
* id of the node running this destination. The per-entry origin
|
|
17
|
+
* (`tags.agentId`) is preferred so forwarded agent logs are attributed to
|
|
18
|
+
* their source node rather than the hub that ships them.
|
|
19
|
+
*/
|
|
15
20
|
readonly node: string;
|
|
16
21
|
/** Extra static labels from configuration. Values must be low-cardinality. */
|
|
17
22
|
readonly extra?: Readonly<Record<string, string>>;
|
|
@@ -37,6 +42,12 @@ export declare function formatLokiLine(entry: LogEntry): string;
|
|
|
37
42
|
* Group a batch into one stream per distinct label set, and sort each stream's
|
|
38
43
|
* values by timestamp.
|
|
39
44
|
*
|
|
45
|
+
* The `node` label is resolved PER ENTRY from `tags.agentId` (the emitting
|
|
46
|
+
* agent, falling back to `labels.node` when absent), so a batch that mixes
|
|
47
|
+
* hub-local lines with forwarded agent lines fans out into one stream per
|
|
48
|
+
* origin node — the whole point of shipping the cluster's logs through the hub
|
|
49
|
+
* without flattening every line onto `node="hub"`.
|
|
50
|
+
*
|
|
40
51
|
* The sort is not cosmetic. Loki historically rejected a stream whose entries
|
|
41
52
|
* were not in ascending time order, and even with out-of-order ingestion enabled
|
|
42
53
|
* it is bounded by a window. Entries reach a destination from several concurrent
|