Skip to content

Commit cec6770

Browse files
authored
Add Compact Console Log Record Exporter for Lambda Environment (#236)
*Issue #, if available:* *Description of changes:* By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 03bdef0 commit cec6770

File tree

3 files changed

+179
-2
lines changed

3 files changed

+179
-2
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ import {
5454
SpanProcessor,
5555
TraceIdRatioBasedSampler,
5656
} from '@opentelemetry/sdk-trace-base';
57-
5857
import {
5958
BatchLogRecordProcessor,
6059
ConsoleLogRecordExporter,
@@ -82,6 +81,7 @@ import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys';
8281
import { AwsCloudWatchOtlpBatchLogRecordProcessor } from './exporter/otlp/aws/logs/aws-cw-otlp-batch-log-record-processor';
8382
import { ConsoleEMFExporter } from './exporter/aws/metrics/console-emf-exporter';
8483
import { EMFExporterBase } from './exporter/aws/metrics/emf-exporter-base';
84+
import { CompactConsoleLogRecordExporter } from './exporter/console/logs/compact-console-log-exporter';
8585

8686
const AWS_TRACES_OTLP_ENDPOINT_PATTERN = '^https://xray\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/traces$';
8787
const AWS_LOGS_OTLP_ENDPOINT_PATTERN = '^https://logs\\.([a-z0-9-]+)\\.amazonaws\\.com/v1/logs$';
@@ -613,7 +613,16 @@ export class AwsLoggerProcessorProvider {
613613
}
614614
}
615615
} else if (exporter === 'console') {
616-
exporters.push(new ConsoleLogRecordExporter());
616+
let logExporter: LogRecordExporter | undefined = undefined;
617+
if (isLambdaEnvironment()) {
618+
diag.debug(
619+
'Lambda environment detected, using CompactConsoleLogRecordExporter instead of ConsoleLogRecordExporter'
620+
);
621+
logExporter = new CompactConsoleLogRecordExporter();
622+
} else {
623+
logExporter = new ConsoleLogRecordExporter();
624+
}
625+
exporters.push(logExporter);
617626
} else {
618627
diag.warn(`Unsupported OTEL_LOGS_EXPORTER value: "${exporter}". Supported values are: otlp, console, none.`);
619628
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { ExportResult, ExportResultCode } from '@opentelemetry/core';
4+
import { ConsoleLogRecordExporter, ReadableLogRecord } from '@opentelemetry/sdk-logs';
5+
6+
export class CompactConsoleLogRecordExporter extends ConsoleLogRecordExporter {
7+
override export(logs: ReadableLogRecord[], resultCallback: (result: ExportResult) => void): void {
8+
this._sendLogRecordsToLambdaConsole(logs, resultCallback);
9+
}
10+
11+
private _sendLogRecordsToLambdaConsole(logRecords: ReadableLogRecord[], done?: (result: ExportResult) => void): void {
12+
for (const logRecord of logRecords) {
13+
process.stdout.write(JSON.stringify(this['_exportInfo'](logRecord)) + '\n');
14+
}
15+
done?.({ code: ExportResultCode.SUCCESS });
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { expect } from 'expect';
5+
import * as sinon from 'sinon';
6+
import { ExportResultCode } from '@opentelemetry/core';
7+
import { ReadableLogRecord } from '@opentelemetry/sdk-logs';
8+
import { Resource } from '@opentelemetry/resources';
9+
import { CompactConsoleLogRecordExporter } from '../../../../src/exporter/console/logs/compact-console-log-exporter';
10+
import { Attributes } from '@opentelemetry/api';
11+
12+
describe('CompactConsoleLogRecordExporter', () => {
13+
let exporter: CompactConsoleLogRecordExporter;
14+
let stdoutWriteSpy: sinon.SinonSpy;
15+
16+
const createMockLogRecord = (body: string, attributes: Attributes = {}): ReadableLogRecord => ({
17+
hrTime: [1640995200, 0],
18+
hrTimeObserved: [1640995200, 0],
19+
body,
20+
severityText: 'INFO',
21+
attributes,
22+
resource: Resource.empty(),
23+
instrumentationScope: { name: 'test', version: '1.0.0' },
24+
droppedAttributesCount: 0,
25+
});
26+
27+
beforeEach(() => {
28+
exporter = new CompactConsoleLogRecordExporter();
29+
stdoutWriteSpy = sinon.spy(process.stdout, 'write');
30+
});
31+
32+
afterEach(() => {
33+
sinon.restore();
34+
});
35+
36+
it('should export logs and call callback with success', done => {
37+
const mockLogRecord = createMockLogRecord('test log message');
38+
const logs = [mockLogRecord];
39+
40+
exporter.export(logs, result => {
41+
expect(result.code).toBe(ExportResultCode.SUCCESS);
42+
expect(stdoutWriteSpy.calledOnce).toBeTruthy();
43+
44+
const writtenData = stdoutWriteSpy.firstCall.args[0];
45+
expect(writtenData).toBe(
46+
'{"resource":{"attributes":{}},"instrumentationScope":{"name":"test","version":"1.0.0"},"timestamp":1640995200000000,"severityText":"INFO","body":"test log message","attributes":{}}\n'
47+
);
48+
49+
const loggedContent = JSON.parse(writtenData);
50+
expect(loggedContent.body).toBe('test log message');
51+
expect(loggedContent.severityText).toBe('INFO');
52+
expect(loggedContent.instrumentationScope.name).toBe('test');
53+
expect(loggedContent.instrumentationScope.version).toBe('1.0.0');
54+
55+
done();
56+
});
57+
});
58+
59+
it('should export multiple logs', done => {
60+
const mockLogRecords = [createMockLogRecord('log 1'), createMockLogRecord('log 2')];
61+
62+
exporter.export(mockLogRecords, result => {
63+
expect(result.code).toBe(ExportResultCode.SUCCESS);
64+
expect(stdoutWriteSpy.callCount).toBe(2);
65+
66+
const firstWrittenData = stdoutWriteSpy.firstCall.args[0];
67+
const secondWrittenData = stdoutWriteSpy.secondCall.args[0];
68+
expect(firstWrittenData).toBe(
69+
'{"resource":{"attributes":{}},"instrumentationScope":{"name":"test","version":"1.0.0"},"timestamp":1640995200000000,"severityText":"INFO","body":"log 1","attributes":{}}\n'
70+
);
71+
expect(secondWrittenData).toBe(
72+
'{"resource":{"attributes":{}},"instrumentationScope":{"name":"test","version":"1.0.0"},"timestamp":1640995200000000,"severityText":"INFO","body":"log 2","attributes":{}}\n'
73+
);
74+
75+
const firstLogContent = JSON.parse(firstWrittenData);
76+
const secondLogContent = JSON.parse(secondWrittenData);
77+
78+
expect(firstLogContent.body).toBe('log 1');
79+
expect(secondLogContent.body).toBe('log 2');
80+
81+
done();
82+
});
83+
});
84+
85+
it('should handle empty logs array', done => {
86+
exporter.export([], result => {
87+
expect(result.code).toBe(ExportResultCode.SUCCESS);
88+
expect(stdoutWriteSpy.called).toBeFalsy();
89+
done();
90+
});
91+
});
92+
93+
it('should work without callback', () => {
94+
const mockLogRecord = createMockLogRecord('test log message');
95+
96+
expect(() => {
97+
exporter.export([mockLogRecord], () => {});
98+
}).not.toThrow();
99+
expect(stdoutWriteSpy.calledOnce).toBeTruthy();
100+
101+
const writtenData = stdoutWriteSpy.firstCall.args[0];
102+
expect(writtenData).toBe(
103+
'{"resource":{"attributes":{}},"instrumentationScope":{"name":"test","version":"1.0.0"},"timestamp":1640995200000000,"severityText":"INFO","body":"test log message","attributes":{}}\n'
104+
);
105+
106+
const loggedContent = JSON.parse(writtenData);
107+
expect(loggedContent.body).toBe('test log message');
108+
});
109+
110+
it('should handle undefined callback gracefully', () => {
111+
const mockLogRecord = createMockLogRecord('test log message');
112+
113+
expect(() => {
114+
exporter['_sendLogRecordsToLambdaConsole']([mockLogRecord]);
115+
}).not.toThrow();
116+
expect(stdoutWriteSpy.calledOnce).toBeTruthy();
117+
118+
const writtenData = stdoutWriteSpy.firstCall.args[0];
119+
expect(writtenData).toBe(
120+
'{"resource":{"attributes":{}},"instrumentationScope":{"name":"test","version":"1.0.0"},"timestamp":1640995200000000,"severityText":"INFO","body":"test log message","attributes":{}}\n'
121+
);
122+
123+
const loggedContent = JSON.parse(writtenData);
124+
expect(loggedContent.body).toBe('test log message');
125+
});
126+
127+
it('should format log record with all expected fields', done => {
128+
const mockLogRecord = createMockLogRecord('detailed test message', {
129+
customKey: 'customValue',
130+
requestId: '12345',
131+
});
132+
133+
exporter.export([mockLogRecord], result => {
134+
expect(result.code).toBe(ExportResultCode.SUCCESS);
135+
136+
const writtenData = stdoutWriteSpy.firstCall.args[0];
137+
expect(writtenData).toBe(
138+
'{"resource":{"attributes":{}},"instrumentationScope":{"name":"test","version":"1.0.0"},"timestamp":1640995200000000,"severityText":"INFO","body":"detailed test message","attributes":{"customKey":"customValue","requestId":"12345"}}\n'
139+
);
140+
141+
const loggedContent = JSON.parse(writtenData);
142+
expect(loggedContent.body).toBe('detailed test message');
143+
expect(loggedContent.severityText).toBe('INFO');
144+
expect(loggedContent.instrumentationScope.name).toBe('test');
145+
expect(loggedContent.instrumentationScope.version).toBe('1.0.0');
146+
expect(loggedContent.attributes).toEqual({ customKey: 'customValue', requestId: '12345' });
147+
148+
done();
149+
});
150+
});
151+
});

0 commit comments

Comments
 (0)