Skip to content

Commit 5cc3c3a

Browse files
committed
feat: configuration of metric export interval via environment variables
Plus: integration test for metrics.
1 parent c7fbb09 commit 5cc3c3a

File tree

5 files changed

+134
-10
lines changed

5 files changed

+134
-10
lines changed

test/integration/ChildProcessWrapper.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,17 @@ export function defaultAppConfiguration(appPort: number): ChildProcessWrapperOpt
172172
env: {
173173
...process.env,
174174
PORT: appPort.toString(),
175+
175176
// have the Node.js SDK send spans every 20 ms instead of every 5 seconds to speed up tests
176177
OTEL_BSP_SCHEDULE_DELAY: '20',
178+
177179
// have the Node.js SDK send logs every 20 ms instead of every 5 seconds to speed up tests
178180
OTEL_BLRP_SCHEDULE_DELAY: '20',
181+
182+
// have the Node.js SDK send metrics every 100 ms instead of every 60 seconds to speed up tests
183+
OTEL_METRIC_EXPORT_INTERVAL: '100',
184+
OTEL_METRIC_EXPORT_TIMEOUT: '90',
185+
179186
DASH0_OTEL_COLLECTOR_BASE_URL: 'http://localhost:4318',
180187
},
181188
};

test/integration/test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ import semver from 'semver';
99

1010
import { SeverityNumber } from '../collector/types/opentelemetry/proto/logs/v1/logs';
1111
import delay from '../util/delay';
12-
import { expectLogRecordAttribute, expectResourceAttribute, expectSpanAttribute } from '../util/expectAttribute';
12+
import {
13+
expectLogRecordAttribute,
14+
expectMetricDataPointAttribute,
15+
expectResourceAttribute,
16+
expectSpanAttribute,
17+
} from '../util/expectAttribute';
1318
import { expectMatchingLogRecord } from '../util/expectMatchingLogRecord';
19+
import { expectMatchingMetric } from '../util/expectMatchingMetric';
1420
import { expectMatchingSpan, expectMatchingSpanInFileDump } from '../util/expectMatchingSpan';
1521
import runCommand from '../util/runCommand';
1622
import waitUntil from '../util/waitUntil';
@@ -71,6 +77,33 @@ describe('attach', () => {
7177
});
7278
});
7379

80+
it('should attach via --require and capture metrics', async () => {
81+
await waitUntil(async () => {
82+
const metrics = await sendRequestAndWaitForMetrics();
83+
expectMatchingMetric(
84+
metrics,
85+
[
86+
resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'),
87+
resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'),
88+
resource => expectResourceAttribute(resource, 'telemetry.distro.name', 'dash0-nodejs'),
89+
resource => expectResourceAttribute(resource, 'telemetry.distro.version', expectedDistroVersion),
90+
],
91+
[
92+
metric => expect(metric.name).to.equal('http.server.duration'),
93+
metric => {
94+
const dataPoints = metric.histogram?.data_points;
95+
expect(dataPoints).to.exist;
96+
expect(dataPoints).to.not.be.empty;
97+
dataPoints?.forEach(dataPoint => {
98+
expectMetricDataPointAttribute(dataPoint, 'http.method', 'GET');
99+
expectMetricDataPointAttribute(dataPoint, 'http.route', '/ohai');
100+
});
101+
},
102+
],
103+
);
104+
});
105+
});
106+
74107
it('should attach via --require and capture logs', async () => {
75108
await waitUntil(async () => {
76109
const logs = await sendRequestAndWaitForLogRecords();
@@ -377,6 +410,11 @@ describe('attach', () => {
377410
return waitForTraceData();
378411
}
379412

413+
async function sendRequestAndWaitForMetrics() {
414+
await sendRequestAndVerifyResponse();
415+
return waitForMetrics();
416+
}
417+
380418
async function sendRequestAndWaitForLogRecords() {
381419
await sendRequestAndVerifyResponse();
382420
return waitForLogRecords();
@@ -396,6 +434,13 @@ describe('attach', () => {
396434
return (await collector().fetchTelemetry()).traces;
397435
}
398436

437+
async function waitForMetrics() {
438+
if (!(await collector().hasMetrics())) {
439+
throw new Error('The collector never received any metrics.');
440+
}
441+
return (await collector().fetchTelemetry()).metrics;
442+
}
443+
399444
async function waitForLogRecords() {
400445
if (!(await collector().hasLogs())) {
401446
throw new Error('The collector never received any log records.');

test/util/expectAttribute.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { KeyValue } from '../collector/types/opentelemetry/proto/common/v1/commo
66
import { Resource } from '../collector/types/opentelemetry/proto/resource/v1/resource';
77
import { Span } from '../collector/types/opentelemetry/proto/trace/v1/trace';
88
import { LogRecord } from '../collector/types/opentelemetry/proto/logs/v1/logs';
9+
import { HistogramDataPoint } from '../collector/types/opentelemetry/proto/metrics/v1/metrics';
910

1011
const { fail } = expect;
1112

@@ -42,6 +43,10 @@ export function expectSpanAttribute(span: Span, key: string, expectedValue: any)
4243
expectAttribute(span, key, expectedValue, 'span');
4344
}
4445

46+
export function expectMetricDataPointAttribute(dataPoint: HistogramDataPoint, key: string, expectedValue: any) {
47+
expectAttribute(dataPoint, key, expectedValue, 'log record');
48+
}
49+
4550
export function expectLogRecordAttribute(logRecord: LogRecord, key: string, expectedValue: any) {
4651
expectAttribute(logRecord, key, expectedValue, 'log record');
4752
}

test/util/expectMatchingMetric.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { ExportMetricsServiceRequest } from '../collector/types/opentelemetry/proto/collector/metrics/v1/metrics_service';
5+
import { ResourceMetrics, ScopeMetrics, Metric } from '../collector/types/opentelemetry/proto/metrics/v1/metrics';
6+
import { Resource } from '../collector/types/opentelemetry/proto/resource/v1/resource';
7+
import {
8+
Expectation,
9+
findMatchingItemsInServiceRequest,
10+
processFindItemsResult,
11+
ServiceRequestMapper,
12+
} from './findMatchingItems';
13+
14+
class MetricsServiceRequestMapper
15+
implements ServiceRequestMapper<ExportMetricsServiceRequest, ResourceMetrics, ScopeMetrics, Metric>
16+
{
17+
getResourceItems(serviceRequest: ExportMetricsServiceRequest): ResourceMetrics[] {
18+
return serviceRequest.resource_metrics;
19+
}
20+
21+
getResource(resourceMetrics: ResourceMetrics): Resource | undefined {
22+
return resourceMetrics.resource;
23+
}
24+
25+
getScopeItems(resourceMetrics: ResourceMetrics): ScopeMetrics[] {
26+
return resourceMetrics.scope_metrics;
27+
}
28+
29+
getItems(scopeMetrics: ScopeMetrics): Metric[] {
30+
return scopeMetrics.metrics;
31+
}
32+
}
33+
34+
export function expectMatchingMetric(
35+
metrics: ExportMetricsServiceRequest[],
36+
resourceExpectations: Expectation<Resource>[],
37+
metricExpectations: Expectation<Metric>[],
38+
): Metric {
39+
const matchResult = findMatchingItemsInServiceRequest(
40+
metrics,
41+
new MetricsServiceRequestMapper(),
42+
resourceExpectations,
43+
metricExpectations,
44+
);
45+
return processFindItemsResult(matchResult, 'metric');
46+
}

test/util/waitUntil.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,23 @@ import isCi from 'is-ci';
66
import delay from './delay';
77

88
export interface RetryOptions {
9-
attempts: number;
109
maxAttempts: number;
1110
waitBetweenRetries: number;
1211
}
1312

13+
export interface RetryInProgress {
14+
attempts: number;
15+
options: RetryOptions;
16+
}
17+
1418
export function defaultRetryOptions(): RetryOptions {
1519
if (isCi) {
1620
return {
17-
attempts: 0,
1821
maxAttempts: 30,
1922
waitBetweenRetries: 300,
2023
};
2124
} else {
2225
return {
23-
attempts: 0,
2426
maxAttempts: 15,
2527
waitBetweenRetries: 200,
2628
};
@@ -33,16 +35,35 @@ export function defaultRetryOptions(): RetryOptions {
3335
* @param fn the function to retry
3436
* @param retryOptions the options for retrying
3537
*/
36-
export default async function waitUntil(fn: () => any, options?: RetryOptions) {
37-
options = options ?? defaultRetryOptions();
38+
export default async function waitUntil(fn: () => any, opts?: Partial<RetryOptions>) {
39+
let retryInProgress: RetryInProgress;
40+
const defaults = defaultRetryOptions();
41+
if (!opts) {
42+
retryInProgress = {
43+
attempts: 0,
44+
options: defaults,
45+
};
46+
} else {
47+
retryInProgress = {
48+
attempts: 0,
49+
options: {
50+
maxAttempts: opts.maxAttempts ?? defaults.maxAttempts,
51+
waitBetweenRetries: opts.waitBetweenRetries ?? defaults.waitBetweenRetries,
52+
},
53+
};
54+
}
55+
return _waitUntil(fn, retryInProgress);
56+
}
57+
58+
async function _waitUntil(fn: () => any, retryInProgress: RetryInProgress) {
3859
try {
3960
return await fn();
4061
} catch (e) {
41-
await delay(options.waitBetweenRetries);
42-
options.attempts += 1;
43-
if (options.attempts > options.maxAttempts) {
62+
await delay(retryInProgress.options.waitBetweenRetries);
63+
retryInProgress.attempts += 1;
64+
if (retryInProgress.attempts > retryInProgress.options.maxAttempts) {
4465
throw e;
4566
}
46-
return waitUntil(fn, options);
67+
return _waitUntil(fn, retryInProgress);
4768
}
4869
}

0 commit comments

Comments
 (0)