@autter/otlp-ingester 1.9.0 → 1.10.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.
@@ -169,6 +169,14 @@
169
169
  "ingester": "1.9.0",
170
170
  "migrations": ["0021-occurrence-collapsed-count"],
171
171
  "sdks": {}
172
+ },
173
+ {
174
+ "id": "edge_request_counts",
175
+ "title": "Edge requests counted from their request summaries",
176
+ "ingester": "1.10.0",
177
+ "migrations": [],
178
+ "route": "/v1/logs",
179
+ "sdks": { "@autter/runtime-edge": "1.3.0" }
172
180
  }
173
181
  ]
174
182
  }
@@ -48,9 +48,40 @@ export interface PromotionLookup {
48
48
  */
49
49
  export declare function promotionLookups(occurrences: RuntimeOccurrenceInput[], now?: number): PromotionLookup[];
50
50
  /**
51
- * One error per promoted occurrence, in `error_count` only — the same
52
- * counting the trace path applies to errors outside a request (a log record
53
- * names no request the rollups count), so error counters for logger-only and
54
- * edge services don't read 0 and no request is invented.
51
+ * `autter.request.counted_by = "summary"`: the request this record belongs to
52
+ * is counted in the usage rollups by its request summary (the record of
53
+ * `autter.operation.kind = "request"` carrying the same mark), because no
54
+ * other feed sees it. `@autter/runtime-edge` (1.3.0+) puts it on every
55
+ * record of a request: an edge function has no trace pipeline and no
56
+ * `http.server.duration` histogram, so without it edge services had error
57
+ * counts and no requests. Nothing else sets it: Node and Next.js summaries
58
+ * never carry it (their requests are counted by the histogram or the spans),
59
+ * so a request is counted by its summary only when its sender says the
60
+ * summary is the one count it gets.
55
61
  */
56
- export declare function promotionRollups(occurrences: RuntimeOccurrence[]): RuntimeMetricPoint[];
62
+ export declare const COUNTED_BY_ATTRIBUTE = "autter.request.counted_by";
63
+ /** On a counted summary: an error of the request was reported (an
64
+ * `autter.capture.mode = "log"` record), which makes it a failed request. */
65
+ export declare const ERROR_REPORTED_ATTRIBUTE = "autter.request.error_reported";
66
+ export declare function countedBySummary(attributes: Record<string, unknown> | null | undefined): boolean;
67
+ /**
68
+ * The usage rollups (`runtime_metrics_1m`) a /v1/logs batch adds, counted the
69
+ * way the trace path counts (normalize-otlp.ts): `request_count` counts
70
+ * requests only, `error_count` counts failed requests once and errors outside
71
+ * a counted request one each.
72
+ *
73
+ * - A request summary marked counted (`countedBySummary`) is one request:
74
+ * `request_count` 1, its duration, on its route and minute; `error_count` 1
75
+ * when it failed — outcome `failed`, a status ≥ 500, or an error reported
76
+ * in it (`autter.request.error_reported`), as an error occurrence recorded
77
+ * in a request fails it on the trace path.
78
+ * - A promoted occurrence (logger-only or edge error record) adds
79
+ * `error_count` 1 — unless its record is marked counted: its request's
80
+ * summary already counts that failure once, however many errors it had.
81
+ *
82
+ * The decision is per record, never by matching records across a batch, so
83
+ * a request whose records an exporter splits over two batches is still
84
+ * counted once. Unmarked summaries add nothing (they are counted elsewhere,
85
+ * or by no one, as before).
86
+ */
87
+ export declare function logRollups(rows: RuntimeLogRecord[], promoted: RuntimeOccurrence[]): RuntimeMetricPoint[];
@@ -81,38 +81,93 @@ export function promotionLookups(occurrences, now = Date.now()) {
81
81
  return lookups;
82
82
  }
83
83
  /**
84
- * One error per promoted occurrence, in `error_count` only — the same
85
- * counting the trace path applies to errors outside a request (a log record
86
- * names no request the rollups count), so error counters for logger-only and
87
- * edge services don't read 0 and no request is invented.
84
+ * `autter.request.counted_by = "summary"`: the request this record belongs to
85
+ * is counted in the usage rollups by its request summary (the record of
86
+ * `autter.operation.kind = "request"` carrying the same mark), because no
87
+ * other feed sees it. `@autter/runtime-edge` (1.3.0+) puts it on every
88
+ * record of a request: an edge function has no trace pipeline and no
89
+ * `http.server.duration` histogram, so without it edge services had error
90
+ * counts and no requests. Nothing else sets it: Node and Next.js summaries
91
+ * never carry it (their requests are counted by the histogram or the spans),
92
+ * so a request is counted by its summary only when its sender says the
93
+ * summary is the one count it gets.
88
94
  */
89
- export function promotionRollups(occurrences) {
95
+ export const COUNTED_BY_ATTRIBUTE = "autter.request.counted_by";
96
+ /** On a counted summary: an error of the request was reported (an
97
+ * `autter.capture.mode = "log"` record), which makes it a failed request. */
98
+ export const ERROR_REPORTED_ATTRIBUTE = "autter.request.error_reported";
99
+ export function countedBySummary(attributes) {
100
+ return attributes?.[COUNTED_BY_ATTRIBUTE] === "summary";
101
+ }
102
+ /**
103
+ * The usage rollups (`runtime_metrics_1m`) a /v1/logs batch adds, counted the
104
+ * way the trace path counts (normalize-otlp.ts): `request_count` counts
105
+ * requests only, `error_count` counts failed requests once and errors outside
106
+ * a counted request one each.
107
+ *
108
+ * - A request summary marked counted (`countedBySummary`) is one request:
109
+ * `request_count` 1, its duration, on its route and minute; `error_count` 1
110
+ * when it failed — outcome `failed`, a status ≥ 500, or an error reported
111
+ * in it (`autter.request.error_reported`), as an error occurrence recorded
112
+ * in a request fails it on the trace path.
113
+ * - A promoted occurrence (logger-only or edge error record) adds
114
+ * `error_count` 1 — unless its record is marked counted: its request's
115
+ * summary already counts that failure once, however many errors it had.
116
+ *
117
+ * The decision is per record, never by matching records across a batch, so
118
+ * a request whose records an exporter splits over two batches is still
119
+ * counted once. Unmarked summaries add nothing (they are counted elsewhere,
120
+ * or by no one, as before).
121
+ */
122
+ export function logRollups(rows, promoted) {
90
123
  const rollups = new Map();
91
- for (const occ of occurrences) {
92
- const bucketAt = new Date(Math.floor(occ.occurredAt.getTime() / 60_000) * 60_000);
93
- const route = normalizeRoute(occ.route);
94
- const key = [
95
- occ.service,
96
- occ.environment,
97
- occ.release ?? "",
98
- route,
99
- bucketAt.getTime(),
100
- ].join(" ");
124
+ const add = (point) => {
125
+ const { at, ...rest } = point;
126
+ const bucketAt = new Date(Math.floor(at.getTime() / 60_000) * 60_000);
127
+ const key = [rest.service, rest.environment, rest.release ?? "", rest.route, bucketAt.getTime()].join(" ");
101
128
  const existing = rollups.get(key);
102
129
  if (existing) {
103
- existing.errorCount += 1;
104
- continue;
130
+ existing.requestCount += rest.requestCount;
131
+ existing.errorCount += rest.errorCount;
132
+ existing.durationSumMs += rest.durationSumMs;
133
+ return;
105
134
  }
106
- rollups.set(key, {
135
+ rollups.set(key, { ...rest, bucketAt, sessionCount: 0 });
136
+ };
137
+ for (const row of rows) {
138
+ if (row.kind !== "request" || !countedBySummary(row.attributes))
139
+ continue;
140
+ const failed = row.outcome === "failed" ||
141
+ row.statusCode >= 500 ||
142
+ row.attributes[ERROR_REPORTED_ATTRIBUTE] === true;
143
+ add({
144
+ service: row.service,
145
+ environment: row.environment,
146
+ release: row.release || null,
147
+ // Already normalized (logs.ts), the key promoted errors use too.
148
+ route: row.route,
149
+ at: row.occurredAt,
150
+ requestCount: 1,
151
+ errorCount: failed ? 1 : 0,
152
+ durationSumMs: row.durationMs,
153
+ });
154
+ }
155
+ for (const occ of promoted) {
156
+ // Its request's summary counts this failure.
157
+ if (countedBySummary(occ.attributes))
158
+ continue;
159
+ // One error of its own, in `error_count` only — what the trace path
160
+ // does for an error outside a request: the record names no request
161
+ // the rollups count, so no request is invented.
162
+ add({
107
163
  service: occ.service,
108
164
  environment: occ.environment,
109
165
  release: occ.release,
110
- route,
111
- bucketAt,
166
+ route: normalizeRoute(occ.route),
167
+ at: occ.occurredAt,
112
168
  requestCount: 0,
113
169
  errorCount: 1,
114
170
  durationSumMs: 0,
115
- sessionCount: 0,
116
171
  });
117
172
  }
118
173
  return [...rollups.values()];
package/dist/server.js CHANGED
@@ -8,7 +8,7 @@ import { deriveFields, occurrenceIdFor } from "./fingerprint.js";
8
8
  import { occurrenceIdentity } from "./fingerprint-v3.js";
9
9
  import { parseStackFrames } from "./stack-frames.js";
10
10
  import { SourceMapCache, Symbolicator } from "./symbolicate.js";
11
- import { logPromotionCandidates, promotionLookups, promotionRollups, } from "./log-promotion.js";
11
+ import { logPromotionCandidates, promotionLookups, logRollups, } from "./log-promotion.js";
12
12
  import { normalizeBrowserPayload, parseBrowserPayload, } from "./normalize-browser.js";
13
13
  import { normalizeMetrics, normalizeTraces, } from "./normalize-otlp.js";
14
14
  import { decodeMetricsRequest, decodeTraceRequest, decodeLogsRequest, encodeExportResponse } from "./otlp-proto.js";
@@ -700,7 +700,7 @@ export function createIngesterApp(config) {
700
700
  // and those are promoted to occurrences here — see log-promotion.ts.
701
701
  const promoted = await promoteLogs(ctx, logs);
702
702
  // Rollups count every promoted error, collapsed or suppressed ones too.
703
- const metricPoints = promotionRollups(promoted);
703
+ const metricPoints = logRollups(logs, promoted);
704
704
  const admitted = admit(ctx, req, promoted);
705
705
  try {
706
706
  await Promise.all([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autter/otlp-ingester",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Self-hostable OTLP + browser-error ingest service for Autter Runtime: normalises telemetry into a per-repo ClickHouse data model",
5
5
  "license": "MIT",
6
6
  "type": "module",