Skip to content

Commit 77c604e

Browse files
authored
Merge pull request #1 from stockstory/feat/powertools-structured-logging
Add support for aws-lambda-powertools-typescript structured logs
2 parents f17b7a3 + 08239de commit 77c604e

3 files changed

Lines changed: 95 additions & 6 deletions

File tree

.idea/jsLinters/eslint.xml

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

src/forwarders/logtail.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fetchMock from 'jest-fetch-mock';
22
import { Request } from 'node-fetch';
33
import { either as E } from 'fp-ts';
4-
import { logtailLogForwarder } from '~/forwarders/logtail';
4+
import { logtailLogForwarder, parseMessageWithPowertoolsLogFormat } from '~/forwarders/logtail';
55
import { FunctionLogEvent } from '~/aws/events';
66

77
describe('test logtail log forwarding', () => {
@@ -72,3 +72,40 @@ describe('test logtail log forwarding', () => {
7272
expect(listener.logsQueue.length).toBe(3);
7373
});
7474
});
75+
76+
describe('test parseMessageWithPowertoolsLogFormat`', () => {
77+
test('should succeed and return parsed result', () => {
78+
const log = JSON.stringify({
79+
level: 'INFO',
80+
message: 'An event occurred',
81+
service: 'mylambda-dev-doathing',
82+
timestamp: '2023-01-18T01:28:02.072Z',
83+
someProperty: 'hello',
84+
anotherProperty: ['test'],
85+
});
86+
87+
const result = parseMessageWithPowertoolsLogFormat(log);
88+
89+
expect(result).toStrictEqual({
90+
_tag: 'Right',
91+
right: {
92+
level: 'INFO',
93+
message: 'An event occurred',
94+
service: 'mylambda-dev-doathing',
95+
timestamp: '2023-01-18T01:28:02.072Z',
96+
someProperty: 'hello',
97+
anotherProperty: ['test'],
98+
},
99+
});
100+
});
101+
102+
test('should fail when message is not valid', () => {
103+
const log = JSON.stringify({
104+
message: 'An event occurred',
105+
date: '2023-01-18T01:28:02.072Z',
106+
});
107+
const result = parseMessageWithPowertoolsLogFormat(log);
108+
109+
expect(E.isLeft(result)).toStrictEqual(true);
110+
});
111+
});

src/forwarders/logtail.ts

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,39 @@
11
import fetch from 'node-fetch';
22
import { FunctionLogEvent } from '~/aws/events';
3-
import { function as F, array as A } from 'fp-ts';
3+
import { function as F, array as A, either as E } from 'fp-ts';
4+
import { z } from 'zod';
5+
6+
// @see https://awslabs.github.io/aws-lambda-powertools-typescript/latest/core/logger/#standard-structured-keys
7+
export const powertoolsLogSchema = z
8+
.object({
9+
level: z.string(), // Logging level set for the Lambda function's invocation
10+
message: z.string(), // A descriptive, human-readable representation of this log item
11+
timestamp: z.string(), // Timestamp string in simplified extended ISO format (ISO 8601)
12+
service: z.string(), // A unique name identifier of the service this Lambda function belongs to, by default service_undefined
13+
xray_trace_id: z.string().optional(), // X-Ray Trace ID. Always present in Lambda environment, but not sent locally.
14+
sampling_rate: z.number().optional(), // When enabled, it prints all the logs of a percentage of invocations, e.g. 10%
15+
error: z.string().optional(), // Optional - An object containing information about the Error passed to the logger
16+
})
17+
.passthrough();
18+
19+
export type PowertoolsLogRecord = z.infer<typeof powertoolsLogSchema>;
20+
21+
export const parseMessageWithPowertoolsLogFormat = (message: string): E.Either<Error, PowertoolsLogRecord> =>
22+
F.pipe(
23+
E.tryCatch(
24+
(): unknown => JSON.parse(message),
25+
(reason) => new Error(`String is not JSON: ${reason}`),
26+
),
27+
E.chain((entry) => {
28+
const result = powertoolsLogSchema.safeParse(entry);
29+
30+
if (result.success) {
31+
return E.right(result.data);
32+
} else {
33+
return E.left(new Error(`Message is not in lambda powertools format`));
34+
}
35+
}),
36+
);
437

538
export const logtailLogForwarder =
639
(token: string, ingestionUrl: string, listener: { logsQueue: FunctionLogEvent[] }) => (): Promise<void> => {
@@ -15,10 +48,23 @@ export const logtailLogForwarder =
1548
body: JSON.stringify(
1649
F.pipe(
1750
logs,
18-
A.map((log) => ({
19-
dt: log.time,
20-
message: log.record,
21-
})),
51+
A.map((log) =>
52+
F.pipe(
53+
log.record,
54+
parseMessageWithPowertoolsLogFormat,
55+
E.fold(
56+
() => ({
57+
dt: log.time,
58+
message: log.record,
59+
}),
60+
({ message, ...data }) => ({
61+
dt: log.time,
62+
message,
63+
data,
64+
}),
65+
),
66+
),
67+
),
2268
),
2369
),
2470
headers: {

0 commit comments

Comments
 (0)