apitally 1.0.0-beta.2 → 1.0.0-beta.4

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.
Files changed (74) hide show
  1. package/README.md +7 -25
  2. package/dist/activation.cjs +2 -1
  3. package/dist/activation.cjs.map +1 -1
  4. package/dist/activation.d.cts +1 -0
  5. package/dist/activation.d.ts +1 -0
  6. package/dist/activation.js +2 -1
  7. package/dist/activation.js.map +1 -1
  8. package/dist/consumers.cjs +96 -0
  9. package/dist/consumers.cjs.map +1 -0
  10. package/dist/consumers.d.cts +13 -0
  11. package/dist/consumers.d.ts +13 -0
  12. package/dist/consumers.js +93 -0
  13. package/dist/consumers.js.map +1 -0
  14. package/dist/context.cjs +2 -8
  15. package/dist/context.cjs.map +1 -1
  16. package/dist/context.d.cts +5 -5
  17. package/dist/context.d.ts +5 -5
  18. package/dist/context.js +3 -7
  19. package/dist/context.js.map +1 -1
  20. package/dist/exportWorker.cjs +6 -5
  21. package/dist/exportWorker.cjs.map +1 -1
  22. package/dist/exportWorker.js +6 -5
  23. package/dist/exportWorker.js.map +1 -1
  24. package/dist/index.cjs +2 -2
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/metrics.cjs +164 -32
  31. package/dist/metrics.cjs.map +1 -1
  32. package/dist/metrics.d.cts +4 -6
  33. package/dist/metrics.d.ts +4 -6
  34. package/dist/metrics.js +165 -33
  35. package/dist/metrics.js.map +1 -1
  36. package/dist/nestjs/index.cjs +6 -0
  37. package/dist/nestjs/index.cjs.map +1 -1
  38. package/dist/nestjs/index.js +6 -0
  39. package/dist/nestjs/index.js.map +1 -1
  40. package/dist/providers.cjs +0 -8
  41. package/dist/providers.cjs.map +1 -1
  42. package/dist/providers.d.cts +0 -2
  43. package/dist/providers.d.ts +0 -2
  44. package/dist/providers.js +1 -8
  45. package/dist/providers.js.map +1 -1
  46. package/dist/requestObservation.cjs +6 -4
  47. package/dist/requestObservation.cjs.map +1 -1
  48. package/dist/requestObservation.js +7 -5
  49. package/dist/requestObservation.js.map +1 -1
  50. package/dist/serverErrors.cjs +30 -28
  51. package/dist/serverErrors.cjs.map +1 -1
  52. package/dist/serverErrors.js +30 -28
  53. package/dist/serverErrors.js.map +1 -1
  54. package/dist/spanProcessor.cjs +0 -2
  55. package/dist/spanProcessor.cjs.map +1 -1
  56. package/dist/spanProcessor.js +0 -2
  57. package/dist/spanProcessor.js.map +1 -1
  58. package/dist/spool.cjs +5 -0
  59. package/dist/spool.cjs.map +1 -1
  60. package/dist/spool.d.cts +2 -1
  61. package/dist/spool.d.ts +2 -1
  62. package/dist/spool.js +5 -0
  63. package/dist/spool.js.map +1 -1
  64. package/dist/validationErrors.cjs +29 -26
  65. package/dist/validationErrors.cjs.map +1 -1
  66. package/dist/validationErrors.js +29 -26
  67. package/dist/validationErrors.js.map +1 -1
  68. package/package.json +2 -2
  69. package/dist/consumer.cjs +0 -61
  70. package/dist/consumer.cjs.map +0 -1
  71. package/dist/consumer.d.cts +0 -12
  72. package/dist/consumer.d.ts +0 -12
  73. package/dist/consumer.js +0 -59
  74. package/dist/consumer.js.map +0 -1
package/dist/metrics.cjs CHANGED
@@ -1,29 +1,43 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_providers = require("./providers.cjs");
2
+ const require_logger = require("./logger.cjs");
3
+ let _opentelemetry_api = require("@opentelemetry/api");
4
+ let _opentelemetry_core = require("@opentelemetry/core");
3
5
  let _opentelemetry_otlp_transformer = require("@opentelemetry/otlp-transformer");
4
6
  let node_os = require("node:os");
5
7
  let _opentelemetry_sdk_metrics = require("@opentelemetry/sdk-metrics");
6
8
  //#region src/metrics.ts
9
+ const HISTOGRAM_SCALE = 3;
10
+ const SCALE_FACTOR = Math.LOG2E * 2 ** HISTOGRAM_SCALE;
11
+ const MAX_COMBINATIONS = 5e4;
12
+ const COMBINATIONS_PER_REQUEST = 1e3;
13
+ const CAPACITY_WARNING = `Apitally request metrics exceeded the capacity of ${MAX_COMBINATIONS.toLocaleString("en-US")} distinct attribute combinations per collection interval, so some request metrics are missing.`;
14
+ const HISTOGRAM_METRICS = [
15
+ [
16
+ "http.server.request.duration",
17
+ "s",
18
+ "duration"
19
+ ],
20
+ [
21
+ "http.server.request.body.size",
22
+ "By",
23
+ "requestBodySize"
24
+ ],
25
+ [
26
+ "http.server.response.body.size",
27
+ "By",
28
+ "responseBodySize"
29
+ ]
30
+ ];
7
31
  var MetricsPipeline = class {
8
- meterProvider;
9
- reader;
32
+ resource;
10
33
  spool;
11
- requestDuration;
12
- requestBodySize;
13
- responseBodySize;
34
+ combinations = /* @__PURE__ */ new Map();
35
+ intervalStartTime = (0, _opentelemetry_core.millisToHrTime)(Date.now());
14
36
  lastCpuUsage = process.cpuUsage();
15
37
  lastCpuUsageTimeMillis = performance.now();
16
38
  constructor(resource, spool) {
39
+ this.resource = resource;
17
40
  this.spool = spool;
18
- this.reader = new OnDemandMetricReader();
19
- this.meterProvider = require_providers.createMeterProvider(resource, [this.reader]);
20
- const meter = this.meterProvider.getMeter("apitally");
21
- this.requestDuration = meter.createHistogram("http.server.request.duration", { unit: "s" });
22
- this.requestBodySize = meter.createHistogram("http.server.request.body.size", { unit: "By" });
23
- this.responseBodySize = meter.createHistogram("http.server.response.body.size", { unit: "By" });
24
- meter.createObservableGauge("process.cpu.utilization", { unit: "1" }).addCallback((result) => result.observe(this.observeCpuUtilization()));
25
- meter.createObservableGauge("process.memory.usage", { unit: "By" }).addCallback((result) => result.observe(process.memoryUsage.rss()));
26
- meter.createObservableGauge("process.uptime", { unit: "s" }).addCallback((result) => result.observe(process.uptime()));
27
41
  }
28
42
  recordFromRequest(record) {
29
43
  if (record.dropReason === "method" || record.dropReason === "scheme") return;
@@ -40,15 +54,114 @@ var MetricsPipeline = class {
40
54
  if (consumer !== void 0) attributes["apitally.consumer.identifier"] = consumer;
41
55
  if (scheme !== void 0) attributes["url.scheme"] = scheme;
42
56
  if (typeof statusCode === "number" && statusCode >= 500) attributes["error.type"] = String(statusCode);
43
- if (typeof record.durationSeconds === "number") this.requestDuration.record(record.durationSeconds, attributes);
57
+ const key = JSON.stringify(attributes);
58
+ let histograms = this.combinations.get(key);
59
+ if (histograms === void 0) {
60
+ if (this.combinations.size >= MAX_COMBINATIONS) {
61
+ require_logger.logWarning(CAPACITY_WARNING);
62
+ return;
63
+ }
64
+ histograms = {
65
+ attributes,
66
+ duration: new ExponentialHistogram(),
67
+ requestBodySize: new ExponentialHistogram(),
68
+ responseBodySize: new ExponentialHistogram()
69
+ };
70
+ this.combinations.set(key, histograms);
71
+ }
72
+ if (typeof record.durationSeconds === "number") histograms.duration.record(record.durationSeconds);
44
73
  const requestBodySize = source["http.request.body.size"];
45
- if (typeof requestBodySize === "number") this.requestBodySize.record(requestBodySize, attributes);
74
+ if (typeof requestBodySize === "number") histograms.requestBodySize.record(requestBodySize);
46
75
  const responseBodySize = source["http.response.body.size"];
47
- if (typeof responseBodySize === "number") this.responseBodySize.record(responseBodySize, attributes);
76
+ if (typeof responseBodySize === "number") histograms.responseBodySize.record(responseBodySize);
48
77
  }
49
78
  async collectAndExport() {
50
- const { resourceMetrics } = await this.reader.collect();
51
- const payload = _opentelemetry_otlp_transformer.ProtobufMetricsSerializer.serializeRequest(resourceMetrics);
79
+ const combinations = [...this.combinations.values()];
80
+ this.combinations = /* @__PURE__ */ new Map();
81
+ const startTime = this.intervalStartTime;
82
+ const endTime = (0, _opentelemetry_core.millisToHrTime)(Date.now());
83
+ this.intervalStartTime = endTime;
84
+ const gauges = [
85
+ [
86
+ "process.cpu.utilization",
87
+ "1",
88
+ this.observeCpuUtilization()
89
+ ],
90
+ [
91
+ "process.memory.usage",
92
+ "By",
93
+ process.memoryUsage.rss()
94
+ ],
95
+ [
96
+ "process.uptime",
97
+ "s",
98
+ process.uptime()
99
+ ]
100
+ ];
101
+ await this.append(gauges.map(([name, unit, value]) => ({
102
+ descriptor: {
103
+ name,
104
+ description: "",
105
+ unit,
106
+ valueType: _opentelemetry_api.ValueType.DOUBLE
107
+ },
108
+ aggregationTemporality: _opentelemetry_sdk_metrics.AggregationTemporality.CUMULATIVE,
109
+ dataPointType: _opentelemetry_sdk_metrics.DataPointType.GAUGE,
110
+ dataPoints: [{
111
+ startTime,
112
+ endTime,
113
+ attributes: {},
114
+ value
115
+ }]
116
+ })));
117
+ for (let index = 0; index < combinations.length; index += COMBINATIONS_PER_REQUEST) {
118
+ const slice = combinations.slice(index, index + COMBINATIONS_PER_REQUEST);
119
+ const metrics = [];
120
+ for (const [name, unit, field] of HISTOGRAM_METRICS) {
121
+ const dataPoints = slice.filter((histograms) => histograms[field].count > 0).map(({ attributes, [field]: histogram }) => ({
122
+ startTime,
123
+ endTime,
124
+ attributes,
125
+ value: {
126
+ count: histogram.count,
127
+ sum: histogram.sum,
128
+ min: histogram.min,
129
+ max: histogram.max,
130
+ scale: HISTOGRAM_SCALE,
131
+ zeroCount: histogram.zeroCount,
132
+ positive: {
133
+ offset: histogram.offset,
134
+ bucketCounts: histogram.bucketCounts
135
+ },
136
+ negative: {
137
+ offset: 0,
138
+ bucketCounts: []
139
+ }
140
+ }
141
+ }));
142
+ if (dataPoints.length > 0) metrics.push({
143
+ descriptor: {
144
+ name,
145
+ description: "",
146
+ unit,
147
+ valueType: _opentelemetry_api.ValueType.DOUBLE
148
+ },
149
+ aggregationTemporality: _opentelemetry_sdk_metrics.AggregationTemporality.DELTA,
150
+ dataPointType: _opentelemetry_sdk_metrics.DataPointType.EXPONENTIAL_HISTOGRAM,
151
+ dataPoints
152
+ });
153
+ }
154
+ await this.append(metrics);
155
+ }
156
+ }
157
+ async append(metrics) {
158
+ const payload = _opentelemetry_otlp_transformer.ProtobufMetricsSerializer.serializeRequest({
159
+ resource: this.resource,
160
+ scopeMetrics: [{
161
+ scope: { name: "apitally" },
162
+ metrics
163
+ }]
164
+ });
52
165
  if (payload) await this.spool.append("metrics", payload);
53
166
  }
54
167
  observeCpuUtilization() {
@@ -61,20 +174,39 @@ var MetricsPipeline = class {
61
174
  return elapsedMicros > 0 ? cpuTimeMicros / elapsedMicros / (0, node_os.availableParallelism)() : 0;
62
175
  }
63
176
  };
64
- var OnDemandMetricReader = class extends _opentelemetry_sdk_metrics.MetricReader {
65
- constructor() {
66
- super({
67
- aggregationSelector: (instrumentType) => instrumentType === _opentelemetry_sdk_metrics.InstrumentType.HISTOGRAM ? { type: _opentelemetry_sdk_metrics.AggregationType.EXPONENTIAL_HISTOGRAM } : { type: _opentelemetry_sdk_metrics.AggregationType.DEFAULT },
68
- aggregationTemporalitySelector: (instrumentType) => instrumentType === _opentelemetry_sdk_metrics.InstrumentType.HISTOGRAM ? _opentelemetry_sdk_metrics.AggregationTemporality.DELTA : _opentelemetry_sdk_metrics.AggregationTemporality.CUMULATIVE
69
- });
70
- }
71
- onForceFlush() {
72
- return Promise.resolve();
73
- }
74
- onShutdown() {
75
- return Promise.resolve();
177
+ var ExponentialHistogram = class {
178
+ count = 0;
179
+ sum = 0;
180
+ min = Number.POSITIVE_INFINITY;
181
+ max = Number.NEGATIVE_INFINITY;
182
+ zeroCount = 0;
183
+ offset = 0;
184
+ bucketCounts = [];
185
+ record(value) {
186
+ this.count++;
187
+ this.sum += value;
188
+ this.min = Math.min(this.min, value);
189
+ this.max = Math.max(this.max, value);
190
+ if (value === 0) {
191
+ this.zeroCount++;
192
+ return;
193
+ }
194
+ const index = mapToIndex(value);
195
+ if (this.bucketCounts.length === 0) this.offset = index;
196
+ else if (index < this.offset) {
197
+ this.bucketCounts.unshift(...new Array(this.offset - index).fill(0));
198
+ this.offset = index;
199
+ }
200
+ const position = index - this.offset;
201
+ while (this.bucketCounts.length <= position) this.bucketCounts.push(0);
202
+ this.bucketCounts[position]++;
76
203
  }
77
204
  };
205
+ function mapToIndex(value) {
206
+ const exponent = Math.floor(Math.log2(value));
207
+ if (2 ** exponent === value) return (exponent << HISTOGRAM_SCALE) - 1;
208
+ return Math.floor(Math.log(value) * SCALE_FACTOR);
209
+ }
78
210
  //#endregion
79
211
  exports.MetricsPipeline = MetricsPipeline;
80
212
 
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.cjs","names":["createMeterProvider","ProtobufMetricsSerializer","availableParallelism","MetricReader","InstrumentType","AggregationType","AggregationTemporality"],"sources":["../src/metrics.ts"],"sourcesContent":["import { availableParallelism } from \"node:os\";\nimport type { Attributes, Histogram } from \"@opentelemetry/api\";\nimport { ProtobufMetricsSerializer } from \"@opentelemetry/otlp-transformer\";\nimport type { Resource } from \"@opentelemetry/resources\";\nimport {\n AggregationTemporality,\n AggregationType,\n InstrumentType,\n type MeterProvider,\n MetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport type { RequestRecord } from \"./context.js\";\nimport { createMeterProvider } from \"./providers.js\";\nimport type { Spool } from \"./spool.js\";\n\n// Request histograms use finalized transport data, independent of span timing and sampling.\nexport class MetricsPipeline {\n readonly meterProvider: MeterProvider;\n readonly reader: MetricReader;\n private readonly spool: Spool;\n private readonly requestDuration: Histogram;\n private readonly requestBodySize: Histogram;\n private readonly responseBodySize: Histogram;\n private lastCpuUsage = process.cpuUsage();\n private lastCpuUsageTimeMillis = performance.now();\n\n constructor(resource: Resource, spool: Spool) {\n this.spool = spool;\n this.reader = new OnDemandMetricReader();\n this.meterProvider = createMeterProvider(resource, [this.reader]);\n const meter = this.meterProvider.getMeter(\"apitally\");\n this.requestDuration = meter.createHistogram(\"http.server.request.duration\", { unit: \"s\" });\n this.requestBodySize = meter.createHistogram(\"http.server.request.body.size\", { unit: \"By\" });\n this.responseBodySize = meter.createHistogram(\"http.server.response.body.size\", { unit: \"By\" });\n meter\n .createObservableGauge(\"process.cpu.utilization\", { unit: \"1\" })\n .addCallback((result) => result.observe(this.observeCpuUtilization()));\n meter\n .createObservableGauge(\"process.memory.usage\", { unit: \"By\" })\n .addCallback((result) => result.observe(process.memoryUsage.rss()));\n // Apitally ingest treats every metrics export as a liveness signal, so this\n // gauge keeps each collection non-empty.\n meter\n .createObservableGauge(\"process.uptime\", { unit: \"s\" })\n .addCallback((result) => result.observe(process.uptime()));\n }\n\n // Excluded and sampled-out requests count; preflight, websocket, and unmatched routes do not.\n recordFromRequest(record: RequestRecord): void {\n if (record.dropReason === \"method\" || record.dropReason === \"scheme\") {\n return;\n }\n const source = record.attributes;\n const method = source[\"http.request.method\"] ?? source[\"http.method\"];\n const scheme = source[\"url.scheme\"] ?? source[\"http.scheme\"];\n const route = source[\"http.route\"];\n if (typeof route !== \"string\" || route === \"\") {\n return;\n }\n const attributes: Attributes = { \"http.route\": route };\n if (method !== undefined) {\n attributes[\"http.request.method\"] = method;\n }\n const statusCode = source[\"http.response.status_code\"] ?? source[\"http.status_code\"];\n if (statusCode !== undefined) {\n attributes[\"http.response.status_code\"] = statusCode;\n }\n const consumer = source[\"apitally.consumer.identifier\"];\n if (consumer !== undefined) {\n attributes[\"apitally.consumer.identifier\"] = consumer;\n }\n if (scheme !== undefined) {\n attributes[\"url.scheme\"] = scheme;\n }\n if (typeof statusCode === \"number\" && statusCode >= 500) {\n attributes[\"error.type\"] = String(statusCode);\n }\n if (typeof record.durationSeconds === \"number\") {\n this.requestDuration.record(record.durationSeconds, attributes);\n }\n const requestBodySize = source[\"http.request.body.size\"];\n if (typeof requestBodySize === \"number\") {\n this.requestBodySize.record(requestBodySize, attributes);\n }\n const responseBodySize = source[\"http.response.body.size\"];\n if (typeof responseBodySize === \"number\") {\n this.responseBodySize.record(responseBodySize, attributes);\n }\n }\n\n async collectAndExport(): Promise<void> {\n const { resourceMetrics } = await this.reader.collect();\n const payload = ProtobufMetricsSerializer.serializeRequest(resourceMetrics);\n if (payload) {\n await this.spool.append(\"metrics\", payload);\n }\n }\n\n private observeCpuUtilization(): number {\n const cpuUsage = process.cpuUsage();\n const nowMillis = performance.now();\n const cpuTimeMicros =\n cpuUsage.user - this.lastCpuUsage.user + (cpuUsage.system - this.lastCpuUsage.system);\n const elapsedMicros = (nowMillis - this.lastCpuUsageTimeMillis) * 1000;\n this.lastCpuUsage = cpuUsage;\n this.lastCpuUsageTimeMillis = nowMillis;\n return elapsedMicros > 0 ? cpuTimeMicros / elapsedMicros / availableParallelism() : 0;\n }\n}\n\n// The export worker collects exponential histograms with delta temporality.\n// Gauges keep the default aggregation and cumulative temporality.\nclass OnDemandMetricReader extends MetricReader {\n constructor() {\n super({\n aggregationSelector: (instrumentType) =>\n instrumentType === InstrumentType.HISTOGRAM\n ? { type: AggregationType.EXPONENTIAL_HISTOGRAM }\n : { type: AggregationType.DEFAULT },\n aggregationTemporalitySelector: (instrumentType) =>\n instrumentType === InstrumentType.HISTOGRAM\n ? AggregationTemporality.DELTA\n : AggregationTemporality.CUMULATIVE,\n });\n }\n\n protected onForceFlush(): Promise<void> {\n return Promise.resolve();\n }\n\n protected onShutdown(): Promise<void> {\n return Promise.resolve();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"metrics.cjs","names":["millisToHrTime","ValueType","AggregationTemporality","DataPointType","ProtobufMetricsSerializer","availableParallelism"],"sources":["../src/metrics.ts"],"sourcesContent":["import { availableParallelism } from \"node:os\";\nimport { type Attributes, ValueType } from \"@opentelemetry/api\";\nimport { millisToHrTime } from \"@opentelemetry/core\";\nimport { ProtobufMetricsSerializer } from \"@opentelemetry/otlp-transformer\";\nimport type { Resource } from \"@opentelemetry/resources\";\nimport {\n AggregationTemporality,\n DataPointType,\n type ExponentialHistogramMetricData,\n type MetricData,\n} from \"@opentelemetry/sdk-metrics\";\nimport type { RequestRecord } from \"./context.js\";\nimport { logWarning } from \"./logger.js\";\nimport type { Spool } from \"./spool.js\";\n\nconst HISTOGRAM_SCALE = 3;\nconst SCALE_FACTOR = Math.LOG2E * 2 ** HISTOGRAM_SCALE;\n// Guards against misuse such as a request ID used as the consumer identifier.\nconst MAX_COMBINATIONS = 50_000;\n// Keeps each appended request well below the spool's 4 MB file rotation threshold.\nconst COMBINATIONS_PER_REQUEST = 1_000;\nconst CAPACITY_WARNING = `Apitally request metrics exceeded the capacity of ${MAX_COMBINATIONS.toLocaleString(\"en-US\")} distinct attribute combinations per collection interval, so some request metrics are missing.`;\nconst HISTOGRAM_METRICS = [\n [\"http.server.request.duration\", \"s\", \"duration\"],\n [\"http.server.request.body.size\", \"By\", \"requestBodySize\"],\n [\"http.server.response.body.size\", \"By\", \"responseBodySize\"],\n] as const;\n\ninterface RequestHistograms {\n attributes: Attributes;\n duration: ExponentialHistogram;\n requestBodySize: ExponentialHistogram;\n responseBodySize: ExponentialHistogram;\n}\n\n// Request histograms use finalized transport data, independent of span timing and sampling.\nexport class MetricsPipeline {\n private readonly resource: Resource;\n private readonly spool: Spool;\n private combinations = new Map<string, RequestHistograms>();\n private intervalStartTime = millisToHrTime(Date.now());\n private lastCpuUsage = process.cpuUsage();\n private lastCpuUsageTimeMillis = performance.now();\n\n constructor(resource: Resource, spool: Spool) {\n this.resource = resource;\n this.spool = spool;\n }\n\n // Excluded and sampled-out requests count; preflight, websocket, and unmatched routes do not.\n recordFromRequest(record: RequestRecord): void {\n if (record.dropReason === \"method\" || record.dropReason === \"scheme\") {\n return;\n }\n const source = record.attributes;\n const method = source[\"http.request.method\"] ?? source[\"http.method\"];\n const scheme = source[\"url.scheme\"] ?? source[\"http.scheme\"];\n const route = source[\"http.route\"];\n if (typeof route !== \"string\" || route === \"\") {\n return;\n }\n const attributes: Attributes = { \"http.route\": route };\n if (method !== undefined) {\n attributes[\"http.request.method\"] = method;\n }\n const statusCode = source[\"http.response.status_code\"] ?? source[\"http.status_code\"];\n if (statusCode !== undefined) {\n attributes[\"http.response.status_code\"] = statusCode;\n }\n const consumer = source[\"apitally.consumer.identifier\"];\n if (consumer !== undefined) {\n attributes[\"apitally.consumer.identifier\"] = consumer;\n }\n if (scheme !== undefined) {\n attributes[\"url.scheme\"] = scheme;\n }\n if (typeof statusCode === \"number\" && statusCode >= 500) {\n attributes[\"error.type\"] = String(statusCode);\n }\n // Attributes are always set in the same order, so equal combinations give equal keys.\n const key = JSON.stringify(attributes);\n let histograms = this.combinations.get(key);\n if (histograms === undefined) {\n if (this.combinations.size >= MAX_COMBINATIONS) {\n logWarning(CAPACITY_WARNING);\n return;\n }\n histograms = {\n attributes,\n duration: new ExponentialHistogram(),\n requestBodySize: new ExponentialHistogram(),\n responseBodySize: new ExponentialHistogram(),\n };\n this.combinations.set(key, histograms);\n }\n if (typeof record.durationSeconds === \"number\") {\n histograms.duration.record(record.durationSeconds);\n }\n const requestBodySize = source[\"http.request.body.size\"];\n if (typeof requestBodySize === \"number\") {\n histograms.requestBodySize.record(requestBodySize);\n }\n const responseBodySize = source[\"http.response.body.size\"];\n if (typeof responseBodySize === \"number\") {\n histograms.responseBodySize.record(responseBodySize);\n }\n }\n\n async collectAndExport(): Promise<void> {\n const combinations = [...this.combinations.values()];\n this.combinations = new Map();\n const startTime = this.intervalStartTime;\n const endTime = millisToHrTime(Date.now());\n this.intervalStartTime = endTime;\n const gauges: [string, string, number][] = [\n [\"process.cpu.utilization\", \"1\", this.observeCpuUtilization()],\n [\"process.memory.usage\", \"By\", process.memoryUsage.rss()],\n [\"process.uptime\", \"s\", process.uptime()],\n ];\n // Apitally ingest treats every metrics export as a liveness signal, so the\n // gauges are appended on every collection.\n await this.append(\n gauges.map(([name, unit, value]) => ({\n descriptor: { name, description: \"\", unit, valueType: ValueType.DOUBLE },\n aggregationTemporality: AggregationTemporality.CUMULATIVE,\n dataPointType: DataPointType.GAUGE,\n dataPoints: [{ startTime, endTime, attributes: {}, value }],\n })),\n );\n // Each combination's histograms stay in one request because Apitally ingest\n // joins them per resource entry.\n for (let index = 0; index < combinations.length; index += COMBINATIONS_PER_REQUEST) {\n const slice = combinations.slice(index, index + COMBINATIONS_PER_REQUEST);\n const metrics: ExponentialHistogramMetricData[] = [];\n for (const [name, unit, field] of HISTOGRAM_METRICS) {\n const dataPoints = slice\n .filter((histograms) => histograms[field].count > 0)\n .map(({ attributes, [field]: histogram }) => ({\n startTime,\n endTime,\n attributes,\n value: {\n count: histogram.count,\n sum: histogram.sum,\n min: histogram.min,\n max: histogram.max,\n scale: HISTOGRAM_SCALE,\n zeroCount: histogram.zeroCount,\n positive: { offset: histogram.offset, bucketCounts: histogram.bucketCounts },\n negative: { offset: 0, bucketCounts: [] },\n },\n }));\n if (dataPoints.length > 0) {\n metrics.push({\n descriptor: { name, description: \"\", unit, valueType: ValueType.DOUBLE },\n aggregationTemporality: AggregationTemporality.DELTA,\n dataPointType: DataPointType.EXPONENTIAL_HISTOGRAM,\n dataPoints,\n });\n }\n }\n await this.append(metrics);\n }\n }\n\n private async append(metrics: MetricData[]): Promise<void> {\n const payload = ProtobufMetricsSerializer.serializeRequest({\n resource: this.resource,\n scopeMetrics: [{ scope: { name: \"apitally\" }, metrics }],\n });\n if (payload) {\n await this.spool.append(\"metrics\", payload);\n }\n }\n\n private observeCpuUtilization(): number {\n const cpuUsage = process.cpuUsage();\n const nowMillis = performance.now();\n const cpuTimeMicros =\n cpuUsage.user - this.lastCpuUsage.user + (cpuUsage.system - this.lastCpuUsage.system);\n const elapsedMicros = (nowMillis - this.lastCpuUsageTimeMillis) * 1000;\n this.lastCpuUsage = cpuUsage;\n this.lastCpuUsageTimeMillis = nowMillis;\n return elapsedMicros > 0 ? cpuTimeMicros / elapsedMicros / availableParallelism() : 0;\n }\n}\n\n// Base-2 exponential histogram at a fixed scale for non-negative values, with the\n// bucket indexes of the OpenTelemetry specification.\nclass ExponentialHistogram {\n count = 0;\n sum = 0;\n min = Number.POSITIVE_INFINITY;\n max = Number.NEGATIVE_INFINITY;\n zeroCount = 0;\n offset = 0;\n bucketCounts: number[] = [];\n\n record(value: number): void {\n this.count++;\n this.sum += value;\n this.min = Math.min(this.min, value);\n this.max = Math.max(this.max, value);\n if (value === 0) {\n this.zeroCount++;\n return;\n }\n const index = mapToIndex(value);\n if (this.bucketCounts.length === 0) {\n this.offset = index;\n } else if (index < this.offset) {\n this.bucketCounts.unshift(...new Array<number>(this.offset - index).fill(0));\n this.offset = index;\n }\n const position = index - this.offset;\n while (this.bucketCounts.length <= position) {\n this.bucketCounts.push(0);\n }\n this.bucketCounts[position]++;\n }\n}\n\nfunction mapToIndex(value: number): number {\n const exponent = Math.floor(Math.log2(value));\n // Exact powers of two are the inclusive upper boundary of the bucket below.\n if (2 ** exponent === value) {\n return (exponent << HISTOGRAM_SCALE) - 1;\n }\n return Math.floor(Math.log(value) * SCALE_FACTOR);\n}\n"],"mappings":"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"}
@@ -1,20 +1,18 @@
1
1
  import { Spool } from "./spool.cjs";
2
2
  import { RequestRecord } from "./context.cjs";
3
3
  import { Resource } from "@opentelemetry/resources";
4
- import { MeterProvider, MetricReader } from "@opentelemetry/sdk-metrics";
5
4
  //#region src/metrics.d.ts
6
5
  export declare class MetricsPipeline {
7
- readonly meterProvider: MeterProvider;
8
- readonly reader: MetricReader;
6
+ private readonly resource;
9
7
  private readonly spool;
10
- private readonly requestDuration;
11
- private readonly requestBodySize;
12
- private readonly responseBodySize;
8
+ private combinations;
9
+ private intervalStartTime;
13
10
  private lastCpuUsage;
14
11
  private lastCpuUsageTimeMillis;
15
12
  constructor(resource: Resource, spool: Spool);
16
13
  recordFromRequest(record: RequestRecord): void;
17
14
  collectAndExport(): Promise<void>;
15
+ private append;
18
16
  private observeCpuUtilization;
19
17
  }
20
18
  //#endregion
package/dist/metrics.d.ts CHANGED
@@ -1,20 +1,18 @@
1
1
  import { Spool } from "./spool.js";
2
2
  import { RequestRecord } from "./context.js";
3
- import { MeterProvider, MetricReader } from "@opentelemetry/sdk-metrics";
4
3
  import { Resource } from "@opentelemetry/resources";
5
4
  //#region src/metrics.d.ts
6
5
  export declare class MetricsPipeline {
7
- readonly meterProvider: MeterProvider;
8
- readonly reader: MetricReader;
6
+ private readonly resource;
9
7
  private readonly spool;
10
- private readonly requestDuration;
11
- private readonly requestBodySize;
12
- private readonly responseBodySize;
8
+ private combinations;
9
+ private intervalStartTime;
13
10
  private lastCpuUsage;
14
11
  private lastCpuUsageTimeMillis;
15
12
  constructor(resource: Resource, spool: Spool);
16
13
  recordFromRequest(record: RequestRecord): void;
17
14
  collectAndExport(): Promise<void>;
15
+ private append;
18
16
  private observeCpuUtilization;
19
17
  }
20
18
  //#endregion
package/dist/metrics.js CHANGED
@@ -1,28 +1,42 @@
1
- import { createMeterProvider } from "./providers.js";
1
+ import { logWarning } from "./logger.js";
2
+ import { ValueType } from "@opentelemetry/api";
3
+ import { millisToHrTime } from "@opentelemetry/core";
2
4
  import { ProtobufMetricsSerializer } from "@opentelemetry/otlp-transformer";
3
5
  import { availableParallelism } from "node:os";
4
- import { AggregationTemporality, AggregationType, InstrumentType, MetricReader } from "@opentelemetry/sdk-metrics";
6
+ import { AggregationTemporality, DataPointType } from "@opentelemetry/sdk-metrics";
5
7
  //#region src/metrics.ts
8
+ const HISTOGRAM_SCALE = 3;
9
+ const SCALE_FACTOR = Math.LOG2E * 2 ** HISTOGRAM_SCALE;
10
+ const MAX_COMBINATIONS = 5e4;
11
+ const COMBINATIONS_PER_REQUEST = 1e3;
12
+ const CAPACITY_WARNING = `Apitally request metrics exceeded the capacity of ${MAX_COMBINATIONS.toLocaleString("en-US")} distinct attribute combinations per collection interval, so some request metrics are missing.`;
13
+ const HISTOGRAM_METRICS = [
14
+ [
15
+ "http.server.request.duration",
16
+ "s",
17
+ "duration"
18
+ ],
19
+ [
20
+ "http.server.request.body.size",
21
+ "By",
22
+ "requestBodySize"
23
+ ],
24
+ [
25
+ "http.server.response.body.size",
26
+ "By",
27
+ "responseBodySize"
28
+ ]
29
+ ];
6
30
  var MetricsPipeline = class {
7
- meterProvider;
8
- reader;
31
+ resource;
9
32
  spool;
10
- requestDuration;
11
- requestBodySize;
12
- responseBodySize;
33
+ combinations = /* @__PURE__ */ new Map();
34
+ intervalStartTime = millisToHrTime(Date.now());
13
35
  lastCpuUsage = process.cpuUsage();
14
36
  lastCpuUsageTimeMillis = performance.now();
15
37
  constructor(resource, spool) {
38
+ this.resource = resource;
16
39
  this.spool = spool;
17
- this.reader = new OnDemandMetricReader();
18
- this.meterProvider = createMeterProvider(resource, [this.reader]);
19
- const meter = this.meterProvider.getMeter("apitally");
20
- this.requestDuration = meter.createHistogram("http.server.request.duration", { unit: "s" });
21
- this.requestBodySize = meter.createHistogram("http.server.request.body.size", { unit: "By" });
22
- this.responseBodySize = meter.createHistogram("http.server.response.body.size", { unit: "By" });
23
- meter.createObservableGauge("process.cpu.utilization", { unit: "1" }).addCallback((result) => result.observe(this.observeCpuUtilization()));
24
- meter.createObservableGauge("process.memory.usage", { unit: "By" }).addCallback((result) => result.observe(process.memoryUsage.rss()));
25
- meter.createObservableGauge("process.uptime", { unit: "s" }).addCallback((result) => result.observe(process.uptime()));
26
40
  }
27
41
  recordFromRequest(record) {
28
42
  if (record.dropReason === "method" || record.dropReason === "scheme") return;
@@ -39,15 +53,114 @@ var MetricsPipeline = class {
39
53
  if (consumer !== void 0) attributes["apitally.consumer.identifier"] = consumer;
40
54
  if (scheme !== void 0) attributes["url.scheme"] = scheme;
41
55
  if (typeof statusCode === "number" && statusCode >= 500) attributes["error.type"] = String(statusCode);
42
- if (typeof record.durationSeconds === "number") this.requestDuration.record(record.durationSeconds, attributes);
56
+ const key = JSON.stringify(attributes);
57
+ let histograms = this.combinations.get(key);
58
+ if (histograms === void 0) {
59
+ if (this.combinations.size >= MAX_COMBINATIONS) {
60
+ logWarning(CAPACITY_WARNING);
61
+ return;
62
+ }
63
+ histograms = {
64
+ attributes,
65
+ duration: new ExponentialHistogram(),
66
+ requestBodySize: new ExponentialHistogram(),
67
+ responseBodySize: new ExponentialHistogram()
68
+ };
69
+ this.combinations.set(key, histograms);
70
+ }
71
+ if (typeof record.durationSeconds === "number") histograms.duration.record(record.durationSeconds);
43
72
  const requestBodySize = source["http.request.body.size"];
44
- if (typeof requestBodySize === "number") this.requestBodySize.record(requestBodySize, attributes);
73
+ if (typeof requestBodySize === "number") histograms.requestBodySize.record(requestBodySize);
45
74
  const responseBodySize = source["http.response.body.size"];
46
- if (typeof responseBodySize === "number") this.responseBodySize.record(responseBodySize, attributes);
75
+ if (typeof responseBodySize === "number") histograms.responseBodySize.record(responseBodySize);
47
76
  }
48
77
  async collectAndExport() {
49
- const { resourceMetrics } = await this.reader.collect();
50
- const payload = ProtobufMetricsSerializer.serializeRequest(resourceMetrics);
78
+ const combinations = [...this.combinations.values()];
79
+ this.combinations = /* @__PURE__ */ new Map();
80
+ const startTime = this.intervalStartTime;
81
+ const endTime = millisToHrTime(Date.now());
82
+ this.intervalStartTime = endTime;
83
+ const gauges = [
84
+ [
85
+ "process.cpu.utilization",
86
+ "1",
87
+ this.observeCpuUtilization()
88
+ ],
89
+ [
90
+ "process.memory.usage",
91
+ "By",
92
+ process.memoryUsage.rss()
93
+ ],
94
+ [
95
+ "process.uptime",
96
+ "s",
97
+ process.uptime()
98
+ ]
99
+ ];
100
+ await this.append(gauges.map(([name, unit, value]) => ({
101
+ descriptor: {
102
+ name,
103
+ description: "",
104
+ unit,
105
+ valueType: ValueType.DOUBLE
106
+ },
107
+ aggregationTemporality: AggregationTemporality.CUMULATIVE,
108
+ dataPointType: DataPointType.GAUGE,
109
+ dataPoints: [{
110
+ startTime,
111
+ endTime,
112
+ attributes: {},
113
+ value
114
+ }]
115
+ })));
116
+ for (let index = 0; index < combinations.length; index += COMBINATIONS_PER_REQUEST) {
117
+ const slice = combinations.slice(index, index + COMBINATIONS_PER_REQUEST);
118
+ const metrics = [];
119
+ for (const [name, unit, field] of HISTOGRAM_METRICS) {
120
+ const dataPoints = slice.filter((histograms) => histograms[field].count > 0).map(({ attributes, [field]: histogram }) => ({
121
+ startTime,
122
+ endTime,
123
+ attributes,
124
+ value: {
125
+ count: histogram.count,
126
+ sum: histogram.sum,
127
+ min: histogram.min,
128
+ max: histogram.max,
129
+ scale: HISTOGRAM_SCALE,
130
+ zeroCount: histogram.zeroCount,
131
+ positive: {
132
+ offset: histogram.offset,
133
+ bucketCounts: histogram.bucketCounts
134
+ },
135
+ negative: {
136
+ offset: 0,
137
+ bucketCounts: []
138
+ }
139
+ }
140
+ }));
141
+ if (dataPoints.length > 0) metrics.push({
142
+ descriptor: {
143
+ name,
144
+ description: "",
145
+ unit,
146
+ valueType: ValueType.DOUBLE
147
+ },
148
+ aggregationTemporality: AggregationTemporality.DELTA,
149
+ dataPointType: DataPointType.EXPONENTIAL_HISTOGRAM,
150
+ dataPoints
151
+ });
152
+ }
153
+ await this.append(metrics);
154
+ }
155
+ }
156
+ async append(metrics) {
157
+ const payload = ProtobufMetricsSerializer.serializeRequest({
158
+ resource: this.resource,
159
+ scopeMetrics: [{
160
+ scope: { name: "apitally" },
161
+ metrics
162
+ }]
163
+ });
51
164
  if (payload) await this.spool.append("metrics", payload);
52
165
  }
53
166
  observeCpuUtilization() {
@@ -60,20 +173,39 @@ var MetricsPipeline = class {
60
173
  return elapsedMicros > 0 ? cpuTimeMicros / elapsedMicros / availableParallelism() : 0;
61
174
  }
62
175
  };
63
- var OnDemandMetricReader = class extends MetricReader {
64
- constructor() {
65
- super({
66
- aggregationSelector: (instrumentType) => instrumentType === InstrumentType.HISTOGRAM ? { type: AggregationType.EXPONENTIAL_HISTOGRAM } : { type: AggregationType.DEFAULT },
67
- aggregationTemporalitySelector: (instrumentType) => instrumentType === InstrumentType.HISTOGRAM ? AggregationTemporality.DELTA : AggregationTemporality.CUMULATIVE
68
- });
69
- }
70
- onForceFlush() {
71
- return Promise.resolve();
72
- }
73
- onShutdown() {
74
- return Promise.resolve();
176
+ var ExponentialHistogram = class {
177
+ count = 0;
178
+ sum = 0;
179
+ min = Number.POSITIVE_INFINITY;
180
+ max = Number.NEGATIVE_INFINITY;
181
+ zeroCount = 0;
182
+ offset = 0;
183
+ bucketCounts = [];
184
+ record(value) {
185
+ this.count++;
186
+ this.sum += value;
187
+ this.min = Math.min(this.min, value);
188
+ this.max = Math.max(this.max, value);
189
+ if (value === 0) {
190
+ this.zeroCount++;
191
+ return;
192
+ }
193
+ const index = mapToIndex(value);
194
+ if (this.bucketCounts.length === 0) this.offset = index;
195
+ else if (index < this.offset) {
196
+ this.bucketCounts.unshift(...new Array(this.offset - index).fill(0));
197
+ this.offset = index;
198
+ }
199
+ const position = index - this.offset;
200
+ while (this.bucketCounts.length <= position) this.bucketCounts.push(0);
201
+ this.bucketCounts[position]++;
75
202
  }
76
203
  };
204
+ function mapToIndex(value) {
205
+ const exponent = Math.floor(Math.log2(value));
206
+ if (2 ** exponent === value) return (exponent << HISTOGRAM_SCALE) - 1;
207
+ return Math.floor(Math.log(value) * SCALE_FACTOR);
208
+ }
77
209
  //#endregion
78
210
  export { MetricsPipeline };
79
211
 
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["import { availableParallelism } from \"node:os\";\nimport type { Attributes, Histogram } from \"@opentelemetry/api\";\nimport { ProtobufMetricsSerializer } from \"@opentelemetry/otlp-transformer\";\nimport type { Resource } from \"@opentelemetry/resources\";\nimport {\n AggregationTemporality,\n AggregationType,\n InstrumentType,\n type MeterProvider,\n MetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport type { RequestRecord } from \"./context.js\";\nimport { createMeterProvider } from \"./providers.js\";\nimport type { Spool } from \"./spool.js\";\n\n// Request histograms use finalized transport data, independent of span timing and sampling.\nexport class MetricsPipeline {\n readonly meterProvider: MeterProvider;\n readonly reader: MetricReader;\n private readonly spool: Spool;\n private readonly requestDuration: Histogram;\n private readonly requestBodySize: Histogram;\n private readonly responseBodySize: Histogram;\n private lastCpuUsage = process.cpuUsage();\n private lastCpuUsageTimeMillis = performance.now();\n\n constructor(resource: Resource, spool: Spool) {\n this.spool = spool;\n this.reader = new OnDemandMetricReader();\n this.meterProvider = createMeterProvider(resource, [this.reader]);\n const meter = this.meterProvider.getMeter(\"apitally\");\n this.requestDuration = meter.createHistogram(\"http.server.request.duration\", { unit: \"s\" });\n this.requestBodySize = meter.createHistogram(\"http.server.request.body.size\", { unit: \"By\" });\n this.responseBodySize = meter.createHistogram(\"http.server.response.body.size\", { unit: \"By\" });\n meter\n .createObservableGauge(\"process.cpu.utilization\", { unit: \"1\" })\n .addCallback((result) => result.observe(this.observeCpuUtilization()));\n meter\n .createObservableGauge(\"process.memory.usage\", { unit: \"By\" })\n .addCallback((result) => result.observe(process.memoryUsage.rss()));\n // Apitally ingest treats every metrics export as a liveness signal, so this\n // gauge keeps each collection non-empty.\n meter\n .createObservableGauge(\"process.uptime\", { unit: \"s\" })\n .addCallback((result) => result.observe(process.uptime()));\n }\n\n // Excluded and sampled-out requests count; preflight, websocket, and unmatched routes do not.\n recordFromRequest(record: RequestRecord): void {\n if (record.dropReason === \"method\" || record.dropReason === \"scheme\") {\n return;\n }\n const source = record.attributes;\n const method = source[\"http.request.method\"] ?? source[\"http.method\"];\n const scheme = source[\"url.scheme\"] ?? source[\"http.scheme\"];\n const route = source[\"http.route\"];\n if (typeof route !== \"string\" || route === \"\") {\n return;\n }\n const attributes: Attributes = { \"http.route\": route };\n if (method !== undefined) {\n attributes[\"http.request.method\"] = method;\n }\n const statusCode = source[\"http.response.status_code\"] ?? source[\"http.status_code\"];\n if (statusCode !== undefined) {\n attributes[\"http.response.status_code\"] = statusCode;\n }\n const consumer = source[\"apitally.consumer.identifier\"];\n if (consumer !== undefined) {\n attributes[\"apitally.consumer.identifier\"] = consumer;\n }\n if (scheme !== undefined) {\n attributes[\"url.scheme\"] = scheme;\n }\n if (typeof statusCode === \"number\" && statusCode >= 500) {\n attributes[\"error.type\"] = String(statusCode);\n }\n if (typeof record.durationSeconds === \"number\") {\n this.requestDuration.record(record.durationSeconds, attributes);\n }\n const requestBodySize = source[\"http.request.body.size\"];\n if (typeof requestBodySize === \"number\") {\n this.requestBodySize.record(requestBodySize, attributes);\n }\n const responseBodySize = source[\"http.response.body.size\"];\n if (typeof responseBodySize === \"number\") {\n this.responseBodySize.record(responseBodySize, attributes);\n }\n }\n\n async collectAndExport(): Promise<void> {\n const { resourceMetrics } = await this.reader.collect();\n const payload = ProtobufMetricsSerializer.serializeRequest(resourceMetrics);\n if (payload) {\n await this.spool.append(\"metrics\", payload);\n }\n }\n\n private observeCpuUtilization(): number {\n const cpuUsage = process.cpuUsage();\n const nowMillis = performance.now();\n const cpuTimeMicros =\n cpuUsage.user - this.lastCpuUsage.user + (cpuUsage.system - this.lastCpuUsage.system);\n const elapsedMicros = (nowMillis - this.lastCpuUsageTimeMillis) * 1000;\n this.lastCpuUsage = cpuUsage;\n this.lastCpuUsageTimeMillis = nowMillis;\n return elapsedMicros > 0 ? cpuTimeMicros / elapsedMicros / availableParallelism() : 0;\n }\n}\n\n// The export worker collects exponential histograms with delta temporality.\n// Gauges keep the default aggregation and cumulative temporality.\nclass OnDemandMetricReader extends MetricReader {\n constructor() {\n super({\n aggregationSelector: (instrumentType) =>\n instrumentType === InstrumentType.HISTOGRAM\n ? { type: AggregationType.EXPONENTIAL_HISTOGRAM }\n : { type: AggregationType.DEFAULT },\n aggregationTemporalitySelector: (instrumentType) =>\n instrumentType === InstrumentType.HISTOGRAM\n ? AggregationTemporality.DELTA\n : AggregationTemporality.CUMULATIVE,\n });\n }\n\n protected onForceFlush(): Promise<void> {\n return Promise.resolve();\n }\n\n protected onShutdown(): Promise<void> {\n return Promise.resolve();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"metrics.js","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["import { availableParallelism } from \"node:os\";\nimport { type Attributes, ValueType } from \"@opentelemetry/api\";\nimport { millisToHrTime } from \"@opentelemetry/core\";\nimport { ProtobufMetricsSerializer } from \"@opentelemetry/otlp-transformer\";\nimport type { Resource } from \"@opentelemetry/resources\";\nimport {\n AggregationTemporality,\n DataPointType,\n type ExponentialHistogramMetricData,\n type MetricData,\n} from \"@opentelemetry/sdk-metrics\";\nimport type { RequestRecord } from \"./context.js\";\nimport { logWarning } from \"./logger.js\";\nimport type { Spool } from \"./spool.js\";\n\nconst HISTOGRAM_SCALE = 3;\nconst SCALE_FACTOR = Math.LOG2E * 2 ** HISTOGRAM_SCALE;\n// Guards against misuse such as a request ID used as the consumer identifier.\nconst MAX_COMBINATIONS = 50_000;\n// Keeps each appended request well below the spool's 4 MB file rotation threshold.\nconst COMBINATIONS_PER_REQUEST = 1_000;\nconst CAPACITY_WARNING = `Apitally request metrics exceeded the capacity of ${MAX_COMBINATIONS.toLocaleString(\"en-US\")} distinct attribute combinations per collection interval, so some request metrics are missing.`;\nconst HISTOGRAM_METRICS = [\n [\"http.server.request.duration\", \"s\", \"duration\"],\n [\"http.server.request.body.size\", \"By\", \"requestBodySize\"],\n [\"http.server.response.body.size\", \"By\", \"responseBodySize\"],\n] as const;\n\ninterface RequestHistograms {\n attributes: Attributes;\n duration: ExponentialHistogram;\n requestBodySize: ExponentialHistogram;\n responseBodySize: ExponentialHistogram;\n}\n\n// Request histograms use finalized transport data, independent of span timing and sampling.\nexport class MetricsPipeline {\n private readonly resource: Resource;\n private readonly spool: Spool;\n private combinations = new Map<string, RequestHistograms>();\n private intervalStartTime = millisToHrTime(Date.now());\n private lastCpuUsage = process.cpuUsage();\n private lastCpuUsageTimeMillis = performance.now();\n\n constructor(resource: Resource, spool: Spool) {\n this.resource = resource;\n this.spool = spool;\n }\n\n // Excluded and sampled-out requests count; preflight, websocket, and unmatched routes do not.\n recordFromRequest(record: RequestRecord): void {\n if (record.dropReason === \"method\" || record.dropReason === \"scheme\") {\n return;\n }\n const source = record.attributes;\n const method = source[\"http.request.method\"] ?? source[\"http.method\"];\n const scheme = source[\"url.scheme\"] ?? source[\"http.scheme\"];\n const route = source[\"http.route\"];\n if (typeof route !== \"string\" || route === \"\") {\n return;\n }\n const attributes: Attributes = { \"http.route\": route };\n if (method !== undefined) {\n attributes[\"http.request.method\"] = method;\n }\n const statusCode = source[\"http.response.status_code\"] ?? source[\"http.status_code\"];\n if (statusCode !== undefined) {\n attributes[\"http.response.status_code\"] = statusCode;\n }\n const consumer = source[\"apitally.consumer.identifier\"];\n if (consumer !== undefined) {\n attributes[\"apitally.consumer.identifier\"] = consumer;\n }\n if (scheme !== undefined) {\n attributes[\"url.scheme\"] = scheme;\n }\n if (typeof statusCode === \"number\" && statusCode >= 500) {\n attributes[\"error.type\"] = String(statusCode);\n }\n // Attributes are always set in the same order, so equal combinations give equal keys.\n const key = JSON.stringify(attributes);\n let histograms = this.combinations.get(key);\n if (histograms === undefined) {\n if (this.combinations.size >= MAX_COMBINATIONS) {\n logWarning(CAPACITY_WARNING);\n return;\n }\n histograms = {\n attributes,\n duration: new ExponentialHistogram(),\n requestBodySize: new ExponentialHistogram(),\n responseBodySize: new ExponentialHistogram(),\n };\n this.combinations.set(key, histograms);\n }\n if (typeof record.durationSeconds === \"number\") {\n histograms.duration.record(record.durationSeconds);\n }\n const requestBodySize = source[\"http.request.body.size\"];\n if (typeof requestBodySize === \"number\") {\n histograms.requestBodySize.record(requestBodySize);\n }\n const responseBodySize = source[\"http.response.body.size\"];\n if (typeof responseBodySize === \"number\") {\n histograms.responseBodySize.record(responseBodySize);\n }\n }\n\n async collectAndExport(): Promise<void> {\n const combinations = [...this.combinations.values()];\n this.combinations = new Map();\n const startTime = this.intervalStartTime;\n const endTime = millisToHrTime(Date.now());\n this.intervalStartTime = endTime;\n const gauges: [string, string, number][] = [\n [\"process.cpu.utilization\", \"1\", this.observeCpuUtilization()],\n [\"process.memory.usage\", \"By\", process.memoryUsage.rss()],\n [\"process.uptime\", \"s\", process.uptime()],\n ];\n // Apitally ingest treats every metrics export as a liveness signal, so the\n // gauges are appended on every collection.\n await this.append(\n gauges.map(([name, unit, value]) => ({\n descriptor: { name, description: \"\", unit, valueType: ValueType.DOUBLE },\n aggregationTemporality: AggregationTemporality.CUMULATIVE,\n dataPointType: DataPointType.GAUGE,\n dataPoints: [{ startTime, endTime, attributes: {}, value }],\n })),\n );\n // Each combination's histograms stay in one request because Apitally ingest\n // joins them per resource entry.\n for (let index = 0; index < combinations.length; index += COMBINATIONS_PER_REQUEST) {\n const slice = combinations.slice(index, index + COMBINATIONS_PER_REQUEST);\n const metrics: ExponentialHistogramMetricData[] = [];\n for (const [name, unit, field] of HISTOGRAM_METRICS) {\n const dataPoints = slice\n .filter((histograms) => histograms[field].count > 0)\n .map(({ attributes, [field]: histogram }) => ({\n startTime,\n endTime,\n attributes,\n value: {\n count: histogram.count,\n sum: histogram.sum,\n min: histogram.min,\n max: histogram.max,\n scale: HISTOGRAM_SCALE,\n zeroCount: histogram.zeroCount,\n positive: { offset: histogram.offset, bucketCounts: histogram.bucketCounts },\n negative: { offset: 0, bucketCounts: [] },\n },\n }));\n if (dataPoints.length > 0) {\n metrics.push({\n descriptor: { name, description: \"\", unit, valueType: ValueType.DOUBLE },\n aggregationTemporality: AggregationTemporality.DELTA,\n dataPointType: DataPointType.EXPONENTIAL_HISTOGRAM,\n dataPoints,\n });\n }\n }\n await this.append(metrics);\n }\n }\n\n private async append(metrics: MetricData[]): Promise<void> {\n const payload = ProtobufMetricsSerializer.serializeRequest({\n resource: this.resource,\n scopeMetrics: [{ scope: { name: \"apitally\" }, metrics }],\n });\n if (payload) {\n await this.spool.append(\"metrics\", payload);\n }\n }\n\n private observeCpuUtilization(): number {\n const cpuUsage = process.cpuUsage();\n const nowMillis = performance.now();\n const cpuTimeMicros =\n cpuUsage.user - this.lastCpuUsage.user + (cpuUsage.system - this.lastCpuUsage.system);\n const elapsedMicros = (nowMillis - this.lastCpuUsageTimeMillis) * 1000;\n this.lastCpuUsage = cpuUsage;\n this.lastCpuUsageTimeMillis = nowMillis;\n return elapsedMicros > 0 ? cpuTimeMicros / elapsedMicros / availableParallelism() : 0;\n }\n}\n\n// Base-2 exponential histogram at a fixed scale for non-negative values, with the\n// bucket indexes of the OpenTelemetry specification.\nclass ExponentialHistogram {\n count = 0;\n sum = 0;\n min = Number.POSITIVE_INFINITY;\n max = Number.NEGATIVE_INFINITY;\n zeroCount = 0;\n offset = 0;\n bucketCounts: number[] = [];\n\n record(value: number): void {\n this.count++;\n this.sum += value;\n this.min = Math.min(this.min, value);\n this.max = Math.max(this.max, value);\n if (value === 0) {\n this.zeroCount++;\n return;\n }\n const index = mapToIndex(value);\n if (this.bucketCounts.length === 0) {\n this.offset = index;\n } else if (index < this.offset) {\n this.bucketCounts.unshift(...new Array<number>(this.offset - index).fill(0));\n this.offset = index;\n }\n const position = index - this.offset;\n while (this.bucketCounts.length <= position) {\n this.bucketCounts.push(0);\n }\n this.bucketCounts[position]++;\n }\n}\n\nfunction mapToIndex(value: number): number {\n const exponent = Math.floor(Math.log2(value));\n // Exact powers of two are the inclusive upper boundary of the bucket below.\n if (2 ** exponent === value) {\n return (exponent << HISTOGRAM_SCALE) - 1;\n }\n return Math.floor(Math.log(value) * SCALE_FACTOR);\n}\n"],"mappings":"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"}
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_packageVersion = require("../packageVersion.cjs");
3
3
  const require_context = require("../context.cjs");
4
4
  const require_exceptions = require("../exceptions.cjs");
5
+ const require_activation = require("../activation.cjs");
5
6
  const require_express_install = require("../express/install.cjs");
6
7
  const require_fastify_install = require("../fastify/install.cjs");
7
8
  let node_module = require("node:module");
@@ -21,6 +22,11 @@ function useApitally(app, options) {
21
22
  const markedInstance = adapterInstance;
22
23
  if (markedInstance[INTERCEPTOR_MARKER] !== true) {
23
24
  app.useGlobalInterceptors(createExceptionInterceptor());
25
+ const close = httpAdapter.close.bind(httpAdapter);
26
+ httpAdapter.close = async () => {
27
+ await close();
28
+ await require_activation.shutdown();
29
+ };
24
30
  markedInstance[INTERCEPTOR_MARKER] = true;
25
31
  }
26
32
  }