@camstack/system 1.2.18 → 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.
@@ -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 byLevel = /* @__PURE__ */ new Map();
69
+ const byStream = /* @__PURE__ */ new Map();
67
70
  for (const entry of entries) {
68
- const values = byLevel.get(entry.level) ?? [];
69
- values.push([toUnixNano(entry.timestamp), formatLokiLine(entry)]);
70
- byLevel.set(entry.level, values);
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 [level, values] of byLevel) streams.push({
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)
@@ -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 byLevel = /* @__PURE__ */ new Map();
64
+ const byStream = /* @__PURE__ */ new Map();
62
65
  for (const entry of entries) {
63
- const values = byLevel.get(entry.level) ?? [];
64
- values.push([toUnixNano(entry.timestamp), formatLokiLine(entry)]);
65
- byLevel.set(entry.level, values);
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 [level, values] of byLevel) streams.push({
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)
@@ -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
- /** This node's id. Bounded by cluster size. */
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.18",
3
+ "version": "1.2.19",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",