Skip to content

Commit bfc26cc

Browse files
authored
feat!: export pino-lambda as a custom destination (#33)
BREAKING CHANGE: Implementation differs from previous version. Please see README.
1 parent d7b94c7 commit bfc26cc

18 files changed

+349
-297
lines changed

.editorconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ indent_size = 2
66
end_of_line = lf
77
indent_style = space
88
insert_final_newline = true
9+
max_line_length = 120
910
trim_trailing_whitespace = true
1011

1112
[*.md]

.github/workflows/pull_request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- name: Setup Node.js
2121
uses: actions/setup-node@v1
2222
with:
23-
node-version: 12
23+
node-version: 14
2424
- name: Install dependencies
2525
run: yarn install --frozen-lockfile
2626
- name: Typescript Build

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
- name: Setup Node.js
1414
uses: actions/setup-node@v1
1515
with:
16-
node-version: 12
16+
node-version: 14
1717
- name: Install dependencies
1818
run: yarn install --frozen-lockfile
1919
- name: Typescript Build

.vscode/launch.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "node",
9+
"request": "attach",
10+
"name": "Attach",
11+
"port": 9229,
12+
"skipFiles": [
13+
"<node_internals>/**"
14+
]
15+
}
16+
]
17+
}

README.md

Lines changed: 79 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,38 @@ pino-lambda
55
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
66
[![Maintenance Status][maintenance-image]](#maintenance-status)
77

8-
A lightweight drop-in decorator for [pino](https://github.com/pinojs/pino) that takes advantage of the unique environment in AWS Lambda functions.
8+
A custom destination for [pino](https://github.com/pinojs/pino) that takes advantage of the unique environment in AWS Lambda functions. [ref](https://github.com/pinojs/pino/blob/master/docs/api.md#destination)
99

10-
By default, this wrapper reformats the log output so it matches the existing Cloudwatch format. The default pino configuration [loses some of the built in support for request ID tracing](https://github.com/pinojs/pino/issues/648) that lambda has built into Cloudwatch insights. This can be disabled or customized as needed.
10+
By default, this destination reformats the log output so it matches the existing Cloudwatch format. The default pino log format [loses some of the built in support for request ID tracing](https://github.com/pinojs/pino/issues/648) that lambda has built into to support Cloudwatch insights and Xray tracing.
1111

12-
It also tracks the request id, correlation ids, and xray tracing from upstream services, and can be set to debug mode by upstream services on a per-request basis.
12+
It also automatically tracks the request id, correlation ids, and xray tracing from upstream services, and can be set to debug mode by upstream services on a per-request basis.
1313

1414
### Conceptually based on the following
1515

1616
- [Capture and forward correlation IDs through different Lambda event sources](https://theburningmonk.com/2017/09/capture-and-forward-correlation-ids-through-different-lambda-event-sources/)
1717
- [Decorated Lambda Handlers](https://tlvince.com/decorated-lambda-handlers)
1818

19-
2019
## Usage
2120

22-
`pino-lambda` is a drop-in replacement for pino. The same configuration and setup can be used without changes.
21+
Basic usage for most applications
2322

2423
```ts
25-
import pino from 'pino-lambda';
26-
const logger = pino();
24+
import pino from 'pino';
25+
import { lambdaRequestTracker, pinoLambdaDestination } from 'pino-lambda';
26+
27+
// custom destination formatter
28+
const destination = pinoLambdaDestination();
29+
const logger = pino({
30+
// typical pino options
31+
}, destination);
32+
const withRequest = lambdaRequestTracker();
2733

2834
async function handler(event, context) {
29-
// new extension added to pino to automatically track requests across all instances of pino
30-
logger.withRequest(event, context);
35+
// automatic request tracing across all instances of pino
36+
// called once at the beginning of your Lambda handler
37+
withRequest(event, context);
3138

32-
// typical logging methods passthrough to pino
39+
// typical logging methods
3340
logger.info({ data: 'Some data' }, 'A log message');
3441
}
3542
```
@@ -50,61 +57,15 @@ other Cloudwatch aware tools such as Datadog and Splunk.
5057
}
5158
```
5259

53-
## Automatic Request ID Tracking
54-
55-
All instances of `pino-lambda` will be automatically log the request id so you don't need to pass an instance of a logger to all of your functions.
56-
57-
```ts
58-
// handler.js
59-
import pino from 'pino-lambda';
60-
const logger = pino();
61-
62-
import { doSomething } from './service';
63-
64-
async function handler(event, context) {
65-
logger.withRequest(event, context);
66-
67-
doSomething();
68-
}
69-
```
70-
71-
A second instance of the pino logger in another file automatically logs the request ID captured by the logger in the above handler.
72-
This alleviates the need to pass an instance of a logger around, or pass the context.
73-
74-
```ts
75-
// service.js
76-
import pino from 'pino-lambda';
77-
const logger = pino();
78-
79-
export function doSomething() {
80-
logger.info({ data: 'Welp' }, 'Another log message');
81-
}
82-
```
83-
84-
Cloudwatch Output
85-
86-
```
87-
2018-12-20T17:05:25.330Z 6fccb00e-0479-11e9-af91-d7ab5c8fe19e INFO A log message
88-
{
89-
"awsRequestId": "6fccb00e-0479-11e9-af91-d7ab5c8fe19e",
90-
"x-correlation-id": "238da608-0542-11e9-8eb2-f2801f1b9fd1",
91-
"x-correlation-trace-id": "Root=1-5c1bcbd2-9cce3b07143efd5bea1224f2;Parent=07adc05e4e92bf13;Sampled=1",
92-
"level": 30,
93-
"message": "A log message",
94-
"data": "Some data"
95-
}
96-
```
97-
9860
## Lambda request tracing
9961

100-
By default, the following event data is tracked for each log statement.
62+
With context tracing enabled, all instances of `pino` that use one of the built in formatters will automatically log the request id and other details so you don't need to pass an instance of a logger to all of your functions.
10163

10264
| Property | Value | Info |
10365
| ------------------------- | ------------------------------------------ | ------------------------------------------------------------------------ |
10466
| awsRequestId | context.awsRequestId | The unique request id for this request |
10567
| apiRequestId | context.requestContext.requestId | The API Gateway RequestId |
10668
| x-correlation-id | event.headers['x-correlation-id'] | The upstream request id for tracing |
107-
| x-correlation-trace-debug | event.headers['x-correlation-debug'] | The upstream service wants debug logs enabled for this request |
10869
| x-correlation-trace-id | process.env._X_AMZN_TRACE_ID | The AWS Xray tracking id |
10970
| x-correlation-\* | event.headers.startsWith('x-correlation-') | Any header that starts with `x-correlation-` will be automatically added |
11071

@@ -124,8 +85,12 @@ This differs from the built in [pino mixin](https://github.com/pinojs/pino/blob/
12485
once per request where the built in pino mixin runs once per log entry.
12586

12687
```ts
127-
import pino from 'pino-lambda';
128-
const logger = pino({
88+
import pino from 'pino';
89+
import { lambdaRequestTracker, pinoLambdaDestination } from 'pino-lambda';
90+
91+
const destination = pinoLambdaDestination();
92+
const logger = pino(destination);
93+
const withRequest = lambdaRequestTracker({
12994
requestMixin: (event, context) => {
13095
return {
13196
// add request header host name
@@ -140,6 +105,11 @@ const logger = pino({
140105
};
141106
}
142107
});
108+
109+
async function handler(event, context) {
110+
withRequest(event, context);
111+
logger.info({ data: 'Some data' }, 'A log message');
112+
}
143113
```
144114

145115
Output
@@ -159,14 +129,26 @@ Output
159129

160130
## Customize output format
161131

162-
If you want the request tracing features, but don't need the Cloudwatch format, you can use the default pino formatter, or supply your own formatter.
132+
By default, the `pinoLambdaDestination` uses the `CloudwatchLogFormatter`. If you want the request tracing features of `pino-lambda`, but don't need the Cloudwatch format, you can use the `PinoLogFormatter` which matches the default object output format of `pino`.
163133

164134
```ts
165-
// default Pino formatter for logs
166-
import pino, { PinoLogFormatter } from 'pino-lambda';
167-
const logger = pino({
135+
import pino from 'pino';
136+
import {
137+
lambdaRequestTracker,
138+
pinoLambdaDestination,
139+
PinoLogFormatter
140+
} from 'pino-lambda';
141+
142+
const destination = pinoLambdaDestination({
168143
formatter: new PinoLogFormatter(),
169144
});
145+
const logger = pino(destination);
146+
const withRequest = lambdaRequestTracker();
147+
148+
async function handler(event, context) {
149+
withRequest(event, context);
150+
logger.info({ data: 'Some data' }, 'A log message');
151+
}
170152
```
171153

172154
Output
@@ -176,27 +158,26 @@ Output
176158
"awsRequestId": "6fccb00e-0479-11e9-af91-d7ab5c8fe19e",
177159
"x-correlation-trace-id": "Root=1-5c1bcbd2-9cce3b07143efd5bea1224f2;Parent=07adc05e4e92bf13;Sampled=1",
178160
"level": 30,
179-
"host": "www.host.com",
180-
"brand": "famicom",
181161
"message": "A log message",
182162
"data": "Some data"
183163
}
184164
```
185165

186-
The formatter function can be replaced with any custom implementation you require by using the supplied interface.
166+
The formatter function can also be replaced with any custom implementation you need by using the supplied interface.
187167

188168
```ts
189-
import pino, { ExtendedPinoLambdaOptions, ILogFormatter } from 'pino-lambda';
169+
import { LogData, ILogFormatter } from 'pino-lambda';
190170

191171
class BananaLogFormatter implements ILogFormatter {
192-
format(buffer: string, options: ExtendedPinoLambdaOptions) {
193-
return `[BANANA] ${buffer}`;
172+
format(data: LogData) {
173+
return `[BANANA] ${JSON.stringify(data)}`;
194174
}
195175
}
196176

197-
const logger = pino({
177+
const destination = pinoLambdaDestination({
198178
formatter: new BananaLogFormatter(),
199179
});
180+
const logger = pino(destination);
200181
```
201182

202183
Output
@@ -207,16 +188,40 @@ Output
207188
"awsRequestId": "6fccb00e-0479-11e9-af91-d7ab5c8fe19e",
208189
"x-correlation-trace-id": "Root=1-5c1bcbd2-9cce3b07143efd5bea1224f2;Parent=07adc05e4e92bf13;Sampled=1",
209190
"level": 30,
210-
"host": "www.host.com",
211-
"brand": "famicom",
212191
"message": "A log message",
213192
"data": "Some data"
214193
}
215194
```
216195

217-
## Downstream Request Debugging
196+
## Best Practices
197+
198+
Unless your application is small, it can be useful to split the logger into its own module for easier reuse across your application code. This ensures that all your logging calls receive the correct formatting and context across the request.
199+
200+
```ts
201+
// logger.ts
202+
import pino from 'pino';
203+
import { pinoLambdaDestination } from 'pino-lambda';
204+
205+
const destination = pinoLambdaDestination();
206+
export const logger = pino(destination);
207+
```
218208

219-
When upstream services invoke a Lambda using `pino-lambda` they can send the `x-correlation-debug` header with a value of `true`. This will enable `debug` logging for that specific request. This is useful for tracing issues across the platform.
209+
```ts
210+
// handler.ts
211+
import { lambdaRequestTracker } from 'pino-lambda';
212+
import { logger } from './logger';
213+
214+
const withRequest = lambdaRequestTracker();
215+
216+
async function handler(event, context) {
217+
// automatic request tracing across all instances of pino
218+
// called once at the beginning of your Lambda handler
219+
withRequest(event, context);
220+
221+
// typical logging methods
222+
logger.info({ data: 'Some data' }, 'A log message');
223+
}
224+
```
220225

221226
## Usage outside of Lambda handlers
222227

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "pino-lambda",
33
"version": "3.0.1",
4-
"description": "Lightweight pino wrapper for AWS Lambda",
4+
"description": "Pino destination formatter for AWS Lambda",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
77
"repository": "https://github.com/formidablelabs/pino-lambda.git",
@@ -17,6 +17,7 @@
1717
"pretest:e2e": "yarn clean && yarn build",
1818
"test": "yarn test:unit",
1919
"test:unit": "tap src/**/*.spec.ts",
20+
"test:debug": "yarn test:unit --node-arg=--inspect-brk --timeout 600",
2021
"test:e2e": "node ./dist/test.js",
2122
"semantic-release": "semantic-release"
2223
},

src/destination.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Writable } from 'stream';
2+
import { GlobalContextStorageProvider } from './context';
3+
import { CloudwatchLogFormatter } from './formatters';
4+
import { PinoLambdaOptions } from './types';
5+
6+
/**
7+
* Custom destination stream for pino
8+
* @param options Options
9+
*/
10+
export const pinoLambdaDestination = (options: PinoLambdaOptions = {}): Writable => {
11+
const writeable = new Writable({
12+
defaultEncoding: 'utf8',
13+
write(chunk, encoding, callback) {
14+
const storageProvider = options.storageProvider || GlobalContextStorageProvider;
15+
const formatter = options?.formatter || new CloudwatchLogFormatter();
16+
17+
const data = JSON.parse(chunk);
18+
const lambdaContext = storageProvider.getContext() || {};
19+
20+
// format the combined request context and log data
21+
let output = formatter.format({ ...lambdaContext, ...data });
22+
23+
// replace characters for proper formatting
24+
output = output.replace(/\n/, '\r');
25+
26+
// final entry must end with carriage return
27+
output += '\n';
28+
29+
if (options.streamWriter) {
30+
options.streamWriter(output);
31+
} else {
32+
process.stdout.write(output);
33+
}
34+
35+
callback();
36+
},
37+
});
38+
39+
return writeable;
40+
};

src/formatters/cloudwatch.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/formatters/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export * from './cloudwatch';
1+
export * from './lambda';
22
export * from './pino';

0 commit comments

Comments
 (0)