@autter/otlp-ingester 1.8.0 → 1.9.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/README.md CHANGED
@@ -57,8 +57,28 @@ AUTTER_INGEST_KEYS='[
57
57
  ```
58
58
 
59
59
  The validator webhook may return the same extra fields:
60
- `{ orgId, repositoryId, scope?, allowedOrigins? }`, with `scope` one of
61
- `server`, `client`, `upload`.
60
+ `{ orgId, repositoryId, scope?, allowedOrigins?, suppression? }`, with
61
+ `scope` one of `server`, `client`, `upload`, and `suppression` the
62
+ repository's suppression rules (1.9.0, below); a static entry takes
63
+ `"suppression": [ … ]` too.
64
+
65
+ ### Volume control (1.9.0)
66
+
67
+ - **Loop collapse.** Per tenant and identity (fingerprints, service,
68
+ environment, release, source, severity, error type and code, normalised
69
+ message and top frames, session), the first `LOOP_COLLAPSE_AFTER`
70
+ occurrences of a minute are stored as usual; the rest of the minute is
71
+ stored as ONE sample whose `collapsed_count` makes the count exact, and
72
+ forwarded to the sink with `count`. A loop of 10,000 a minute is 11 rows,
73
+ not 10,000. Shutdown stores the minute in progress.
74
+ - **Suppression rules** set per repository in Autter: `drop` or `sample`
75
+ (keep `sampleRate`, never a fatal one) the occurrences whose error type,
76
+ message, service, environment, route, release or source match. Delivered
77
+ with the key, applied before anything is stored.
78
+
79
+ Folded and suppressed occurrences are counted in `runtime_ingest_drops`
80
+ (`collapsed`, `suppressed`); request and error-rate rollups still count
81
+ every error. Details: [docs/ARCHITECTURE.md § Volume control](../../docs/ARCHITECTURE.md#volume-control).
62
82
 
63
83
  ### Rejections and drops (1.8.0)
64
84
 
@@ -93,8 +113,8 @@ Autter dashboard reads both. See [docs/COMPATIBILITY.md](../../docs/COMPATIBILIT
93
113
  | `CLICKHOUSE_URL` | — | e.g. `http://localhost:8123`; unset = ingest returns 503 |
94
114
  | `CLICKHOUSE_USER` / `CLICKHOUSE_PASSWORD` | `default` / empty | |
95
115
  | `CLICKHOUSE_DATABASE` | `autter_runtime` | Created automatically |
96
- | `AUTTER_INGEST_KEYS` | — | JSON: `[{"key":"...","orgId":"...","repositoryId":"..."}]` |
97
- | `AUTTER_KEY_VALIDATOR_URL` | — | Webhook: `POST {key}` → `{orgId, repositoryId}` (60 s cache) |
116
+ | `AUTTER_INGEST_KEYS` | — | JSON: `[{"key":"...","orgId":"...","repositoryId":"..."}]` (optional `scope`, `allowedOrigins`, `suppression`) |
117
+ | `AUTTER_KEY_VALIDATOR_URL` | — | Webhook: `POST {key}` → `{orgId, repositoryId, scope?, allowedOrigins?, suppression?}` (60 s cache) |
98
118
  | `AUTTER_KEY_VALIDATOR_TOKEN` | — | Bearer token sent to the validator |
99
119
  | `AUTTER_SINK_URL` | — | Issue-grouping webhook; at-least-once (`docs/ARCHITECTURE.md`) |
100
120
  | `AUTTER_SINK_TOKEN` | — | Bearer token sent to the sink |
@@ -103,6 +123,7 @@ Autter dashboard reads both. See [docs/COMPATIBILITY.md](../../docs/COMPATIBILIT
103
123
  | `SINK_MAX_BUFFERED_MB` | `64` | Sink retry buffer cap (memory) |
104
124
  | `SOURCE_MAP_CACHE_MB` | `64` | Memory budget (decoded size, function scope tables included) for parsed source maps used to symbolicate browser frames at ingest; least recently used maps are evicted, and a map that needs more than half of it is used without being kept |
105
125
  | `MAX_BODY_BYTES` | `1048576` | Request body cap |
126
+ | `LOOP_COLLAPSE_AFTER` | `10` | Identical occurrences stored per minute before the rest of the minute is collapsed into one sample with an exact count; `0` turns loop collapse off |
106
127
  | `RATE_LIMIT_PER_MINUTE` | `300` | Per-key fixed window (server keys) |
107
128
  | `CLIENT_RATE_LIMIT_PER_MINUTE` | `120` | Per-key fixed window (client keys) |
108
129
  | `PROMOTION_LOOKUPS_PER_MINUTE` | `30` | Per-tenant ClickHouse dedupe lookups for `/v1/logs` error promotion; over budget, records are promoted with in-batch dedupe only |
package/dist/auth.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { INGESTER_VERSION_HEADER } from "./compat.js";
2
2
  import { defaultScopeFor, isIngestKeyScope } from "./config.js";
3
3
  import { ingesterVersion } from "./sdk-versions.js";
4
+ import { parseSuppressionRules } from "./suppression.js";
4
5
  const VALIDATOR_CACHE_TTL_MS = 60_000;
5
6
  export class KeyResolver {
6
7
  config;
@@ -12,11 +13,16 @@ export class KeyResolver {
12
13
  const scope = entry.scope ?? defaultScopeFor(entry.key);
13
14
  if (!isIngestKeyScope(scope))
14
15
  continue;
16
+ const suppression = parseSuppressionRules(entry.suppression);
17
+ if (Array.isArray(entry.suppression) && suppression.length < entry.suppression.length) {
18
+ console.error(`AUTTER_INGEST_KEYS: ${entry.suppression.length - suppression.length} invalid suppression rule(s) for repository ${entry.repositoryId} ignored`);
19
+ }
15
20
  this.staticKeys.set(entry.key, {
16
21
  orgId: entry.orgId,
17
22
  repositoryId: entry.repositoryId,
18
23
  scope,
19
24
  allowedOrigins: entry.allowedOrigins ?? [],
25
+ ...(suppression.length ? { suppression } : {}),
20
26
  });
21
27
  }
22
28
  }
@@ -71,11 +77,13 @@ export class KeyResolver {
71
77
  const body = (await response.json());
72
78
  const scope = body.scope === undefined || body.scope === null ? defaultScopeFor(key) : body.scope;
73
79
  if (body.orgId && body.repositoryId && isIngestKeyScope(scope)) {
80
+ const suppression = parseSuppressionRules(body.suppression);
74
81
  ctx = {
75
82
  orgId: body.orgId,
76
83
  repositoryId: body.repositoryId,
77
84
  scope,
78
85
  allowedOrigins: body.allowedOrigins ?? [],
86
+ ...(suppression.length ? { suppression } : {}),
79
87
  };
80
88
  }
81
89
  }
@@ -9,7 +9,7 @@ import { logTableDDL, requestRollupTableDDL, } from "./logs.js";
9
9
  import { ingesterInfoTableDDL, sdkVersionTableDDL, } from "./sdk-versions.js";
10
10
  import { ingestDropsTableDDL } from "./ingest-drops.js";
11
11
  import { inAppFrameResolution } from "./symbolicate.js";
12
- import { MIGRATIONS, OCCURRENCE_CODE_COLUMNS, OCCURRENCE_FRAME_RESOLUTION_COLUMNS, OCCURRENCE_GROUPING_COLUMNS, OCCURRENCE_REQUEST_ID_INDEX, migrationsTableDDL, } from "./migrations.js";
12
+ import { MIGRATIONS, OCCURRENCE_CODE_COLUMNS, OCCURRENCE_COLLAPSE_COLUMNS, OCCURRENCE_FRAME_RESOLUTION_COLUMNS, OCCURRENCE_GROUPING_COLUMNS, OCCURRENCE_REQUEST_ID_INDEX, migrationsTableDDL, } from "./migrations.js";
13
13
  /**
14
14
  * ClickHouse persistence. The schema is idempotent and applied lazily once
15
15
  * per process. Every row is keyed by (org_id, repository_id) — tenant
@@ -113,6 +113,7 @@ export class ClickHouseStore {
113
113
  ${OCCURRENCE_CODE_COLUMNS.join(",\n\t\t\t\t")},
114
114
  ${OCCURRENCE_GROUPING_COLUMNS.join(",\n\t\t\t\t")},
115
115
  ${OCCURRENCE_FRAME_RESOLUTION_COLUMNS.join(",\n\t\t\t\t")},
116
+ ${OCCURRENCE_COLLAPSE_COLUMNS.join(",\n\t\t\t\t")},
116
117
  ${OCCURRENCE_REQUEST_ID_INDEX}
117
118
  )
118
119
  ENGINE = MergeTree
@@ -421,6 +422,8 @@ export class ClickHouseStore {
421
422
  fingerprint_scheme: o.fingerprintScheme,
422
423
  frames: o.frames?.length ? JSON.stringify(o.frames) : "",
423
424
  fingerprints: (o.fingerprints ?? []).map((candidate) => candidate.fingerprint),
425
+ // Migration 0021: only collapsed samples name the column.
426
+ ...(o.collapsedCount ? { collapsed_count: Math.min(0xffff_ffff, Math.max(0, Math.round(o.collapsedCount))) } : {}),
424
427
  })),
425
428
  });
426
429
  }
@@ -155,6 +155,20 @@
155
155
  "ingester": "1.8.0",
156
156
  "migrations": ["0020-occurrence-frame-resolution"],
157
157
  "sdks": {}
158
+ },
159
+ {
160
+ "id": "ingest_suppression",
161
+ "title": "Suppression rules: drop or sample matching errors at ingest",
162
+ "ingester": "1.9.0",
163
+ "migrations": [],
164
+ "sdks": {}
165
+ },
166
+ {
167
+ "id": "loop_collapse",
168
+ "title": "Loop collapse: a looping error stored as one sample a minute with an exact count",
169
+ "ingester": "1.9.0",
170
+ "migrations": ["0021-occurrence-collapsed-count"],
171
+ "sdks": {}
158
172
  }
159
173
  ]
160
174
  }
package/dist/config.d.ts CHANGED
@@ -23,6 +23,13 @@ export interface StaticIngestKey {
23
23
  scope?: IngestKeyScope;
24
24
  /** client keys: exact origins allowed to send (e.g. https://app.example.com). */
25
25
  allowedOrigins?: string[];
26
+ /**
27
+ * Suppression rules for this key's repository (1.9.0, suppression.ts):
28
+ * `[{ "id": "…", "action": "drop" | "sample", "sampleRate"?: 0.1,
29
+ * "filters": [{ "key": "errorType", "op": "equals", "value": "…" }] }]`.
30
+ * Invalid rules are left out (logged once at startup).
31
+ */
32
+ suppression?: unknown[];
26
33
  }
27
34
  export interface IngesterConfig {
28
35
  port: number;
@@ -77,7 +84,15 @@ export interface IngesterConfig {
77
84
  * A map that needs more than half of it is used without being kept.
78
85
  */
79
86
  sourceMapCacheMb?: number;
87
+ /**
88
+ * Loop collapse (1.9.0, loop-collapse.ts): identical occurrences stored
89
+ * per minute before the rest are folded into one sample with an exact
90
+ * count (LOOP_COLLAPSE_AFTER, default 10; 0 turns it off).
91
+ */
92
+ loopCollapseAfter?: number;
80
93
  }
94
+ /** LOOP_COLLAPSE_AFTER: a whole number ≥ 0 (0 = off); anything else is the default, 10. */
95
+ export declare function loopCollapseAfterEnv(raw: string | undefined): number;
81
96
  /** Parse AUTTER_INGEST_KEYS. Entries without a string key, orgId and
82
97
  * repositoryId, or with an unknown scope, are dropped (a typo in `scope`
83
98
  * must not turn a restricted key into a server key). */
package/dist/config.js CHANGED
@@ -12,6 +12,13 @@ export function isIngestKeyScope(value) {
12
12
  export function defaultScopeFor(key) {
13
13
  return key.startsWith(UPLOAD_KEY_PREFIX) ? "upload" : "server";
14
14
  }
15
+ /** LOOP_COLLAPSE_AFTER: a whole number ≥ 0 (0 = off); anything else is the default, 10. */
16
+ export function loopCollapseAfterEnv(raw) {
17
+ if (raw === undefined || raw.trim() === "")
18
+ return 10;
19
+ const value = Number(raw);
20
+ return Number.isInteger(value) && value >= 0 ? value : 10;
21
+ }
15
22
  function intEnv(name, fallback) {
16
23
  const raw = process.env[name];
17
24
  if (!raw)
@@ -92,6 +99,7 @@ export function loadConfig() {
92
99
  redactValuePatterns: patternListEnv("AUTTER_REDACT_VALUE_PATTERNS"),
93
100
  redactKeyPatterns: patternListEnv("AUTTER_REDACT_KEY_PATTERNS"),
94
101
  sourceMapCacheMb: intEnv("SOURCE_MAP_CACHE_MB", 64),
102
+ loopCollapseAfter: loopCollapseAfterEnv(process.env.LOOP_COLLAPSE_AFTER),
95
103
  };
96
104
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(config.clickhouseDatabase)) {
97
105
  throw new Error(`Invalid CLICKHOUSE_DATABASE name: ${config.clickhouseDatabase}`);
package/dist/index.d.ts CHANGED
@@ -2,5 +2,7 @@ export { createIngesterApp } from "./server.js";
2
2
  export { loadConfig } from "./config.js";
3
3
  export * from "./compat.js";
4
4
  export { SinkForwarder, type SinkDrop, type SinkStats, type SinkTuning } from "./sink.js";
5
+ export { collapseIdentity, LoopCollapser, type CollapsedSample, type CollapsePlan, type LoopCollapseOptions, type LoopCollapseStats, } from "./loop-collapse.js";
6
+ export { applySuppression, globMatch, parseSuppressionRules, SUPPRESSION_OPERATORS, SUPPRESSION_PROPERTIES, type SuppressionAction, type SuppressionFilter, type SuppressionOperator, type SuppressionProperty, type SuppressionRule, } from "./suppression.js";
5
7
  export { DROP_REASONS, IngestDropCounter, type DropKind, type DropReason, type DropRoute, type IngestDrop, type IngestDropRow, } from "./ingest-drops.js";
6
8
  export * from "./types.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { loadConfig } from "./config.js";
2
2
  import { createIngesterApp } from "./server.js";
3
3
  import { ingesterVersion } from "./sdk-versions.js";
4
4
  const config = loadConfig();
5
- const { app, store, sink, reportIngesterInfo, drops } = createIngesterApp(config);
5
+ const { app, store, sink, reportIngesterInfo, drops, collapse } = createIngesterApp(config);
6
6
  const server = app.listen(config.port, () => {
7
7
  console.log(`autter otlp-ingester ${ingesterVersion()} listening on :${config.port} ` +
8
8
  `(clickhouse: ${config.clickhouseUrl ? "configured" : "NOT configured"})`);
@@ -25,10 +25,14 @@ async function shutdown(signal) {
25
25
  }
26
26
  }
27
27
  server.close(() => {
28
- // Write the rejection/drop counts still buffered (best effort: a
29
- // failed write is logged, never retried).
30
- void drops
28
+ // Store every loop-collapse window's sample (the minute in progress
29
+ // too), so a deploy loses no folded count; the sink is stopped, so
30
+ // the consumer picks them up from ClickHouse. Then write the
31
+ // rejection/drop counts still buffered (best effort: a failed write
32
+ // is logged, never retried).
33
+ void collapse
31
34
  .stop()
35
+ .finally(() => drops.stop())
32
36
  .finally(() => store.close())
33
37
  .finally(() => process.exit(0));
34
38
  });
@@ -40,5 +44,7 @@ export { createIngesterApp } from "./server.js";
40
44
  export { loadConfig } from "./config.js";
41
45
  export * from "./compat.js";
42
46
  export { SinkForwarder } from "./sink.js";
47
+ export { collapseIdentity, LoopCollapser, } from "./loop-collapse.js";
48
+ export { applySuppression, globMatch, parseSuppressionRules, SUPPRESSION_OPERATORS, SUPPRESSION_PROPERTIES, } from "./suppression.js";
43
49
  export { DROP_REASONS, IngestDropCounter, } from "./ingest-drops.js";
44
50
  export * from "./types.js";
@@ -19,11 +19,24 @@ import type { IngestContext } from "./types.js";
19
19
  * - `unforwarded`: stored in ClickHouse, but the sink webhook gave up on
20
20
  * the batch; the consumer recovers it by replaying ClickHouse.
21
21
  *
22
+ * And two kinds of records left out on purpose (1.9.0, EI-38), counted so
23
+ * the dashboard can explain why stored rows are fewer than records sent:
24
+ * - `collapsed`: a looping error past its first occurrences of the minute
25
+ * (loop-collapse.ts); its payload was not stored, its count is, on the
26
+ * minute's collapsed sample (`collapsed_count`). `records` counts payloads
27
+ * not stored.
28
+ * - `suppressed`: occurrences a suppression rule set by the user dropped or
29
+ * sampled out (suppression.ts).
30
+ *
31
+ * Both are recorded with `requests` 0: no request was refused, and a
32
+ * dashboard before 1.9.0 (which reads an unknown kind as a rejection)
33
+ * finds nothing refused in the last hour.
34
+ *
22
35
  * Counting never slows or fails ingest: `record()` is a synchronous map
23
36
  * update, rows are written in batches every few seconds, a failed write is
24
37
  * logged and dropped (never retried), and the buffer is bounded.
25
38
  */
26
- export type DropKind = "rejected" | "dropped" | "truncated" | "unforwarded";
39
+ export type DropKind = "rejected" | "dropped" | "truncated" | "unforwarded" | "collapsed" | "suppressed";
27
40
  /** Every reason, its kind, and (for rejections) the HTTP status it answers with. */
28
41
  export declare const DROP_REASONS: {
29
42
  /** No key in `Authorization`, `x-autter-key` or `?key=`. */
@@ -146,6 +159,14 @@ export declare const DROP_REASONS: {
146
159
  readonly sink_gave_up: {
147
160
  readonly kind: "unforwarded";
148
161
  };
162
+ /** Occurrences of a looping error past the first LOOP_COLLAPSE_AFTER of the minute. */
163
+ readonly collapsed: {
164
+ readonly kind: "collapsed";
165
+ };
166
+ /** Occurrences a `drop` rule matched, or a `sample` rule sampled out. */
167
+ readonly suppressed: {
168
+ readonly kind: "suppressed";
169
+ };
149
170
  };
150
171
  export type DropReason = keyof typeof DROP_REASONS;
151
172
  /** Routes counted by name; anything else is "other". `sink` is the webhook. */
@@ -56,6 +56,12 @@ export const DROP_REASONS = {
56
56
  sink_rejected: { kind: "unforwarded" },
57
57
  /** SINK_MAX_ATTEMPTS deliveries failed. */
58
58
  sink_gave_up: { kind: "unforwarded" },
59
+ // ── collapsed: counted on the minute's sample, payload not stored (1.9.0) ──
60
+ /** Occurrences of a looping error past the first LOOP_COLLAPSE_AFTER of the minute. */
61
+ collapsed: { kind: "collapsed" },
62
+ // ── suppressed: a suppression rule set by the user left them out (1.9.0) ──
63
+ /** Occurrences a `drop` rule matched, or a `sample` rule sampled out. */
64
+ suppressed: { kind: "suppressed" },
59
65
  };
60
66
  /** Routes counted by name; anything else is "other". `sink` is the webhook. */
61
67
  export const DROP_ROUTES = [
@@ -0,0 +1,144 @@
1
+ import type { DropRoute } from "./ingest-drops.js";
2
+ import type { IngestContext, RuntimeOccurrence } from "./types.js";
3
+ /**
4
+ * Loop collapse (1.9.0, EI-38): an error that repeats in a loop is stored
5
+ * as its first N occurrences of the minute plus ONE sample that carries the
6
+ * exact count of the rest, so a loop of 10,000 events a minute costs N + 1
7
+ * rows a minute, not 10,000, and only duplicate payloads are lost.
8
+ *
9
+ * Identical: same tenant, ingest route and arrival minute, and the same
10
+ * - identity: legacy `fingerprint` and every `fingerprints` candidate;
11
+ * - service, environment, release, source, severity, `expected`;
12
+ * - error type and declared error code;
13
+ * - normalised message (`messageNormalized`: ids, numbers and quoted
14
+ * strings templated out) and normalised top frames (`topFrames`);
15
+ * - session id: an error hitting many sessions is volume, not a loop, and
16
+ * each session must still count as a user.
17
+ * Trace, request and occurrence ids, route, timestamps, attributes and
18
+ * breadcrumbs may differ: they are what a loop varies.
19
+ *
20
+ * Per window (that identity in one arrival minute): the first `after`
21
+ * occurrences (LOOP_COLLAPSE_AFTER, default 10) are stored and forwarded as
22
+ * they arrive. Every later one is folded: not stored on its own, counted.
23
+ * When the minute is over, the window's newest folded occurrence is stored
24
+ * and forwarded as the sample with `collapsedCount` = folded − 1, so the
25
+ * sample stands for every folded occurrence: stored rows + Σ collapsed_count
26
+ * is exactly the number of occurrences accepted. Its time is the newest
27
+ * folded one, so an issue's last-seen stays exact; its weight lands in the
28
+ * minute it arrived in.
29
+ *
30
+ * Exactness under retries: a request's folds are reserved when it is planned
31
+ * and only counted once its ClickHouse writes succeed (`commit`); a request
32
+ * that fails (503, the exporter retries) gives them back (`rollback`). Ids
33
+ * already seen in the window are recognised, so a batch retried after a lost
34
+ * 2xx is not counted twice. A window is flushed only once no request still
35
+ * holds it (or, as a safety net, five minutes after its minute); a fold that
36
+ * commits after that is carried into the next flush. A failed flush write is
37
+ * kept and retried every tick. Shutdown (SIGTERM) flushes every window, so a
38
+ * deploy loses nothing; only a crash loses the folded counts of the minute in
39
+ * progress.
40
+ *
41
+ * Memory is bounded (safety, not a work cap): windows live for their minute
42
+ * and are cleared on the next flush; past `maxWindows` windows a minute, or
43
+ * `maxSamples` held samples, occurrences of new identities pass through
44
+ * uncollapsed (stored as usual, never lost), and past `maxTrackedIds` folded
45
+ * ids a minute, retry recognition stops for new folds.
46
+ */
47
+ export interface LoopCollapseOptions {
48
+ /** Occurrences stored per window before folding starts; 0 turns collapse off. */
49
+ after: number;
50
+ /** Windows (identities) per minute. Default 100,000. */
51
+ maxWindows?: number;
52
+ /** Samples held across all windows until they are flushed. Default 5,000. */
53
+ maxSamples?: number;
54
+ /** Folded occurrence ids remembered per minute, for retry recognition. Default 500,000. */
55
+ maxTrackedIds?: number;
56
+ /** How often due windows are flushed. Default 5 s. */
57
+ flushIntervalMs?: number;
58
+ /** A window still held by a request is flushed anyway this long after its minute. Default 4 min. */
59
+ forceAfterMs?: number;
60
+ now?: () => number;
61
+ log?: (message: string) => void;
62
+ }
63
+ /** One window's sample, ready to store: `sample.collapsedCount` = folded − 1. */
64
+ export interface CollapsedSample {
65
+ route: DropRoute;
66
+ sample: RuntimeOccurrence;
67
+ /** Occurrences the sample stands for (itself included). */
68
+ folded: number;
69
+ }
70
+ /** Store and forward one tenant's samples. A rejection keeps them for the next tick. */
71
+ export type CollapseFlush = (ctx: IngestContext, samples: CollapsedSample[]) => Promise<void>;
72
+ export interface CollapsePlan {
73
+ /** Occurrences to store and forward now. */
74
+ kept: RuntimeOccurrence[];
75
+ /** Occurrences folded into a window's sample by this request. */
76
+ folded: number;
77
+ /** Occurrences recognised as already counted (a retried batch); not stored again. */
78
+ repeated: number;
79
+ /** The request's writes succeeded: count its folds. */
80
+ commit(): void;
81
+ /** The request failed: give its reservations back. */
82
+ rollback(): void;
83
+ }
84
+ export interface LoopCollapseStats {
85
+ windows: number;
86
+ heldSamples: number;
87
+ /** Occurrences folded (committed) since start. */
88
+ folded: number;
89
+ /** Samples stored since start. */
90
+ samplesFlushed: number;
91
+ /** Occurrences that passed through uncollapsed because a bound was reached. */
92
+ overflow: number;
93
+ /** Flush writes that failed (their samples were kept and retried). */
94
+ failedFlushes: number;
95
+ }
96
+ /**
97
+ * The identity two occurrences must share to fold together (see above),
98
+ * hashed: the raw parts can run to kilobytes (frames, messages).
99
+ */
100
+ export declare function collapseIdentity(o: RuntimeOccurrence): string;
101
+ export declare class LoopCollapser {
102
+ private readonly flushSink;
103
+ private readonly minutes;
104
+ /** Folds committed after their window was flushed, and failed flushes. */
105
+ private carry;
106
+ private heldSamples;
107
+ private trackedIds;
108
+ private timer;
109
+ private flushing;
110
+ private lastFailureLog;
111
+ private stats_;
112
+ private readonly after;
113
+ private readonly maxWindows;
114
+ private readonly maxSamples;
115
+ private readonly maxTrackedIds;
116
+ private readonly flushIntervalMs;
117
+ private readonly forceAfterMs;
118
+ private readonly now;
119
+ private readonly log;
120
+ constructor(flushSink: CollapseFlush, options: LoopCollapseOptions);
121
+ get enabled(): boolean;
122
+ /**
123
+ * Split a request's occurrences into those stored now and those folded
124
+ * into their window's sample. Synchronous; never throws (on an internal
125
+ * error every occurrence is kept).
126
+ */
127
+ plan(ctx: IngestContext, route: DropRoute, occurrences: RuntimeOccurrence[]): CollapsePlan;
128
+ private planWindows;
129
+ /** Flush due windows every flushIntervalMs (unref'd). */
130
+ start(): void;
131
+ /** Stop the timer and flush every window, the current minute's too (shutdown). */
132
+ stop(): Promise<void>;
133
+ /**
134
+ * Store the samples of every window whose minute is over and that no
135
+ * request still holds, plus carried folds and earlier failed writes.
136
+ * `all`: every window, current minute included. One flush at a time;
137
+ * never rejects.
138
+ */
139
+ flush(all?: boolean): Promise<void>;
140
+ private collectDue;
141
+ private flushDue;
142
+ private releaseSlot;
143
+ stats(): LoopCollapseStats;
144
+ }