Skip to content

Commit 887aa71

Browse files
committed
feat: collect logs
1 parent ae9fd49 commit 887aa71

File tree

12 files changed

+270
-83
lines changed

12 files changed

+270
-83
lines changed

package-lock.json

Lines changed: 37 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@
4343
"dependencies": {
4444
"@opentelemetry/api": "^1.8.0",
4545
"@opentelemetry/auto-instrumentations-node": "^0.44.0",
46+
"@opentelemetry/exporter-logs-otlp-proto": "^0.51.0",
4647
"@opentelemetry/exporter-metrics-otlp-proto": "^0.51.0",
4748
"@opentelemetry/exporter-trace-otlp-proto": "^0.51.0",
4849
"@opentelemetry/resource-detector-container": "^0.3.11",
4950
"@opentelemetry/resources": "^1.24.0",
51+
"@opentelemetry/sdk-logs": "^0.51.0",
5052
"@opentelemetry/sdk-metrics": "^1.24.0",
5153
"@opentelemetry/sdk-node": "^0.51.0",
5254
"@opentelemetry/sdk-trace-base": "^1.24.0",

src/init.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
import { SpanKind, trace } from '@opentelemetry/api';
55
import { getNodeAutoInstrumentations, getResourceDetectors } from '@opentelemetry/auto-instrumentations-node';
6+
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto';
67
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
78
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
89
import { containerDetector } from '@opentelemetry/resource-detector-container';
910
import { Detector, DetectorSync, envDetector, hostDetector, processDetector, Resource } from '@opentelemetry/resources';
11+
import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
1012
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
1113
import { NodeSDK, NodeSDKConfiguration } from '@opentelemetry/sdk-node';
1214
import { BatchSpanProcessor, SpanProcessor } from '@opentelemetry/sdk-trace-base';
@@ -47,6 +49,12 @@ const spanProcessors: SpanProcessor[] = [
4749
),
4850
];
4951

52+
const logRecordProcessor = new BatchLogRecordProcessor(
53+
new OTLPLogExporter({
54+
url: `${baseUrl}/v1/logs`,
55+
}),
56+
);
57+
5058
if (process.env.DASH0_DEBUG_PRINT_SPANS != null) {
5159
if (process.env.DASH0_DEBUG_PRINT_SPANS.toLocaleLowerCase() === 'true') {
5260
spanProcessors.push(new BatchSpanProcessor(new ConsoleSpanExporter()));
@@ -57,12 +65,15 @@ if (process.env.DASH0_DEBUG_PRINT_SPANS != null) {
5765

5866
const configuration: Partial<NodeSDKConfiguration> = {
5967
spanProcessors: spanProcessors,
68+
6069
metricReader: new PeriodicExportingMetricReader({
6170
exporter: new OTLPMetricExporter({
6271
url: `${baseUrl}/v1/metrics`,
6372
}),
6473
}),
6574

75+
logRecordProcessor,
76+
6677
instrumentations: [getNodeAutoInstrumentations(instrumentationConfig)],
6778

6879
resource: new Resource({

test/apps/express-typescript/app.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import * as logsApi from '@opentelemetry/api-logs';
45
import express, { Express } from 'express';
56

67
import { sendReadyToParentProcess } from '../../util/sendToParentProcess';
78

89
const port: number = parseInt(process.env.PORT || '1302');
910
const app: Express = express();
1011

12+
const logger = logsApi.logs.getLoggerProvider().getLogger('default');
13+
1114
app.get('/ohai', (req, res) => {
15+
logger.emit({
16+
severityNumber: logsApi.SeverityNumber.INFO,
17+
severityText: 'INFO',
18+
body: 'log body',
19+
attributes: { 'log.type': 'LogRecord' },
20+
});
1221
res.json({ message: 'We make Observability easy for every developer.' });
1322
});
1423

test/collector/CollectorChildProcessWrapper.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ export default class CollectorChildProcessWrapper extends ChildProcessWrapper {
1919
return stats.traces >= 1;
2020
}
2121

22+
async hasMetrics() {
23+
const stats = await collector().fetchStats();
24+
return stats.metrics >= 1;
25+
}
26+
27+
async hasLogs() {
28+
const stats = await collector().fetchStats();
29+
return stats.logs >= 1;
30+
}
31+
2232
async hasTelemetry() {
2333
const stats = await collector().fetchStats();
2434
return stats.traces >= 1 || stats.metrics >= 1 || stats.logs >= 1;

test/integration/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
spans.json

test/integration/ChildProcessWrapper.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,10 @@ export function defaultAppConfiguration(appPort: number): ChildProcessWrapperOpt
172172
env: {
173173
...process.env,
174174
PORT: appPort.toString(),
175-
// have the Node.js SDK send spans every 100 ms instead of every 5 seconds to speed up tests
176-
OTEL_BSP_SCHEDULE_DELAY: '100',
175+
// have the Node.js SDK send spans every 20 ms instead of every 5 seconds to speed up tests
176+
OTEL_BSP_SCHEDULE_DELAY: '20',
177+
// have the Node.js SDK send logs every 20 ms instead of every 5 seconds to speed up tests
178+
OTEL_BLRP_SCHEDULE_DELAY: '20',
177179
DASH0_OTEL_COLLECTOR_BASE_URL: 'http://localhost:4318',
178180
},
179181
};

test/integration/test.ts

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import { FileHandle, open, readFile, unlink } from 'node:fs/promises';
77
import { join } from 'node:path';
88
import semver from 'semver';
99

10+
import { SeverityNumber } from '../collector/types/opentelemetry/proto/logs/v1/logs';
1011
import delay from '../util/delay';
11-
12-
import { expectResourceAttribute, expectSpanAttribute } from '../util/expectAttribute';
12+
import { expectLogRecordAttribute, expectResourceAttribute, expectSpanAttribute } from '../util/expectAttribute';
13+
import { expectMatchingLogRecord } from '../util/expectMatchingLogRecord';
1314
import { expectMatchingSpan, expectMatchingSpanInFileDump } from '../util/expectMatchingSpan';
1415
import runCommand from '../util/runCommand';
1516
import waitUntil from '../util/waitUntil';
@@ -38,7 +39,7 @@ describe('attach', () => {
3839
collector().clear();
3940
});
4041

41-
describe('basic tracing', () => {
42+
describe('basic signals', () => {
4243
let appUnderTest: ChildProcessWrapper;
4344

4445
before(async () => {
@@ -53,9 +54,9 @@ describe('attach', () => {
5354

5455
it('should attach via --require and capture spans', async () => {
5556
await waitUntil(async () => {
56-
const telemetry = await sendRequestAndWaitForTraceData();
57+
const traces = await sendRequestAndWaitForTraceData();
5758
expectMatchingSpan(
58-
telemetry.traces,
59+
traces,
5960
[
6061
resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'),
6162
resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'),
@@ -69,6 +70,31 @@ describe('attach', () => {
6970
);
7071
});
7172
});
73+
74+
it('should attach via --require and capture logs', async () => {
75+
await waitUntil(async () => {
76+
const logs = await sendRequestAndWaitForLogRecords();
77+
expectMatchingLogRecord(
78+
logs,
79+
[
80+
resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'),
81+
resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'),
82+
resource => expectResourceAttribute(resource, 'telemetry.distro.name', 'dash0-nodejs'),
83+
resource => expectResourceAttribute(resource, 'telemetry.distro.version', expectedDistroVersion),
84+
],
85+
[
86+
logRecord => expect(logRecord.body).to.deep.equal({ string_value: 'log body' }),
87+
logRecord =>
88+
expect(logRecord.severity_number).to.equal(
89+
SeverityNumber.SEVERITY_NUMBER_INFO,
90+
'severity number should be info',
91+
),
92+
logRecord => expect(logRecord.severity_text).to.equal('INFO'),
93+
logRecord => expectLogRecordAttribute(logRecord, 'log.type', 'LogRecord'),
94+
],
95+
);
96+
});
97+
});
7298
});
7399

74100
describe('pod uid detection', () => {
@@ -87,9 +113,9 @@ describe('attach', () => {
87113

88114
it('should attach via --require and detect the pod uid', async () => {
89115
await waitUntil(async () => {
90-
const telemetry = await sendRequestAndWaitForTraceData();
116+
const traces = await sendRequestAndWaitForTraceData();
91117
expectMatchingSpan(
92-
telemetry.traces,
118+
traces,
93119
[resource => expectResourceAttribute(resource, 'k8s.pod.uid', 'f57400dc-94ce-4806-a52e-d2726f448f15')],
94120
[
95121
span => expect(span.kind).to.equal(SpanKind.SERVER, 'span kind should be server'),
@@ -115,9 +141,9 @@ describe('attach', () => {
115141

116142
it('should attach via --require and derive a service name from the package.json file', async () => {
117143
await waitUntil(async () => {
118-
const telemetry = await sendRequestAndWaitForTraceData();
144+
const traces = await sendRequestAndWaitForTraceData();
119145
expectMatchingSpan(
120-
telemetry.traces,
146+
traces,
121147
[
122148
resource =>
123149
expectResourceAttribute(resource, 'service.name', '[email protected]'),
@@ -150,9 +176,9 @@ describe('attach', () => {
150176
// (because the top level beforeHook is executed after this suite's before hook).
151177
await appUnderTest.start();
152178
await waitUntil(async () => {
153-
const telemetry = await waitForTraceData();
179+
const traces = await waitForTraceData();
154180
expectMatchingSpan(
155-
telemetry.traces,
181+
traces,
156182
[
157183
resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'),
158184
resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'),
@@ -187,9 +213,9 @@ describe('attach', () => {
187213
await appUnderTest.start();
188214
await appUnderTest.stop();
189215
await waitUntil(async () => {
190-
const telemetry = await waitForTraceData();
216+
const traces = await waitForTraceData();
191217
expectMatchingSpan(
192-
telemetry.traces,
218+
traces,
193219
[
194220
resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'),
195221
resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'),
@@ -205,9 +231,9 @@ describe('attach', () => {
205231
await appUnderTest.start();
206232
await appUnderTest.stop('SIGINT');
207233
await waitUntil(async () => {
208-
const telemetry = await waitForTraceData();
234+
const traces = await waitForTraceData();
209235
expectMatchingSpan(
210-
telemetry.traces,
236+
traces,
211237
[
212238
resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'),
213239
resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'),
@@ -241,9 +267,9 @@ describe('attach', () => {
241267
it('should flush telemetry before process exit due to empty event loop', async () => {
242268
await appUnderTest.start();
243269
await waitUntil(async () => {
244-
const telemetry = await waitForTraceData();
270+
const traces = await waitForTraceData();
245271
expectMatchingSpan(
246-
telemetry.traces,
272+
traces,
247273
[
248274
resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'),
249275
resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'),
@@ -351,6 +377,11 @@ describe('attach', () => {
351377
return waitForTraceData();
352378
}
353379

380+
async function sendRequestAndWaitForLogRecords() {
381+
await sendRequestAndVerifyResponse();
382+
return waitForLogRecords();
383+
}
384+
354385
async function sendRequestAndVerifyResponse() {
355386
const response = await fetch(`http://localhost:${appPort}/ohai`);
356387
expect(response.status).to.equal(200);
@@ -362,7 +393,14 @@ describe('attach', () => {
362393
if (!(await collector().hasTraces())) {
363394
throw new Error('The collector never received any spans.');
364395
}
365-
return await collector().fetchTelemetry();
396+
return (await collector().fetchTelemetry()).traces;
397+
}
398+
399+
async function waitForLogRecords() {
400+
if (!(await collector().hasLogs())) {
401+
throw new Error('The collector never received any log records.');
402+
}
403+
return (await collector().fetchTelemetry()).logs;
366404
}
367405

368406
async function verifyFileHasBeenCreated(filename: string): Promise<FileHandle> {

test/util/expectAttribute.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { expect } from 'chai';
55
import { KeyValue } from '../collector/types/opentelemetry/proto/common/v1/common';
66
import { Resource } from '../collector/types/opentelemetry/proto/resource/v1/resource';
77
import { Span } from '../collector/types/opentelemetry/proto/trace/v1/trace';
8+
import { LogRecord } from '../collector/types/opentelemetry/proto/logs/v1/logs';
89

910
const { fail } = expect;
1011

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

45+
export function expectLogRecordAttribute(logRecord: LogRecord, key: string, expectedValue: any) {
46+
expectAttribute(logRecord, key, expectedValue, 'log record');
47+
}
48+
4449
function getValue(attribute: KeyValue) {
4550
const v = attribute.value;
4651
if (v == null) {

0 commit comments

Comments
 (0)