Skip to content

Commit e00e6ca

Browse files
committed
feat!: add maxBufferSize option + update batching API
1 parent 66ccf05 commit e00e6ca

File tree

8 files changed

+236
-55
lines changed

8 files changed

+236
-55
lines changed

README.md

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ import type { LokiOptions } from 'pino-loki'
2020
const transport = pino.transport<LokiOptions>({
2121
target: "pino-loki",
2222
options: {
23-
batching: true,
24-
interval: 5,
25-
2623
host: 'https://my-loki-instance:3100',
2724
basicAuth: {
2825
username: "username",
@@ -102,11 +99,44 @@ If false, errors when sending logs to Loki will be displayed in the console. Def
10299

103100
#### `batching`
104101

105-
Should logs be sent in batch mode. Defaults to `true`.
102+
Batching configuration. When enabled, logs are accumulated in a buffer and sent to Loki at regular intervals, reducing the number of HTTP requests. Batching is **enabled by default**.
103+
104+
```ts
105+
// Batching enabled with default options (interval: 5s, maxBufferSize: 10000)
106+
pinoLoki({ host: '...' })
107+
108+
// Batching with custom options
109+
pinoLoki({
110+
host: '...',
111+
batching: {
112+
interval: 2, // Send logs every 2 seconds
113+
maxBufferSize: 5000 // Keep max 5000 logs in buffer
114+
}
115+
})
116+
117+
// Batching disabled - logs sent immediately
118+
pinoLoki({ host: '...', batching: false })
119+
```
120+
121+
##### `batching.interval`
122+
123+
The interval at which batched logs are sent, in seconds. Defaults to `5`.
106124

107-
#### `interval`
125+
##### `batching.maxBufferSize`
108126

109-
The interval at which batched logs are sent in seconds. Defaults to `5`.
127+
Maximum number of logs to keep in the buffer. When the buffer is full, **oldest logs are dropped** (FIFO) to make room for new ones. Defaults to `10000`.
128+
129+
This prevents memory issues (OOM) if Loki becomes unavailable - without this limit, the buffer would grow indefinitely. Set to `0` for unlimited buffer (probably not really recommended).
130+
131+
```ts
132+
pinoLoki({
133+
host: '...',
134+
batching: {
135+
interval: 10,
136+
maxBufferSize: 50000
137+
}
138+
})
139+
```
110140

111141
#### `replaceTimestamp`
112142

@@ -195,23 +225,23 @@ node foo | pino-loki --hostname=http://hostname:3100
195225
```
196226
$ pino-loki -h
197227
Options:
198-
-V, --version output the version number
199-
-u, --user <user> Loki username
200-
-p, --password <password> Loki password
201-
--hostname <hostname> URL for Loki
202-
--endpoint <endpoint> Path to the Loki push API
203-
--headers <headers> Headers to be sent to Loki (Example: "X-Scope-OrgID=your-id,another-header=another-value")
204-
-b, --batch Should logs be sent in batch mode
205-
-i, --interval <interval> The interval at which batched logs are sent in seconds
206-
-t, --timeout <timeout> Timeout for request to Loki
207-
-s, --silenceErrors If false, errors will be displayed in the console
208-
-r, --replaceTimestamp Replace pino logs timestamps with Date.now()
209-
-l, --labels <label> Additional labels to be added to all Loki logs
210-
-a, --convertArrays If true, arrays will be converted to objects
211-
-pl, --propsLabels <labels> Fields in log line to convert to Loki labels (comma separated values)
212-
--structuredMetaKey <key> Key in the pino log object that contains structured metadata
213-
--no-stdout Disable output to stdout
214-
-h, --help display help for command
228+
-v, --version Print version number and exit
229+
-u, --user <user> Loki username
230+
-p, --password <password> Loki password
231+
--hostname <hostname> URL for Loki (default: http://localhost:3100)
232+
--endpoint <endpoint> Path to the Loki push API (default: /loki/api/v1/push)
233+
--headers <headers> Headers to be sent to Loki (Example: "X-Scope-OrgID=your-id,another=value")
234+
-b, --batching Should logs be sent in batch mode (default: true)
235+
-i, --batching-interval <interval> The interval at which batched logs are sent in seconds (default: 5)
236+
--batching-max-buffer-size <size> Maximum number of logs to buffer (default: 10000, 0 for unlimited)
237+
-t, --timeout <timeout> Timeout for request to Loki in ms (default: 2000)
238+
-s, --silenceErrors If set, errors will not be displayed in the console
239+
-r, --replaceTimestamp Replace pino logs timestamps with Date.now()
240+
-l, --labels <label> Additional labels to be added to all Loki logs (JSON)
241+
--convertArrays If set, arrays will be converted to objects
242+
--propsLabels <labels> Fields in log line to convert to Loki labels (comma separated)
243+
--structuredMetaKey <key> Key in the pino log object that contains structured metadata
244+
-h, --help Print this help message and exit
215245
```
216246

217247
## Examples
@@ -270,7 +300,13 @@ And you should be good to go! You can check our [full example](./examples/adonis
270300
Out-of-order Loki errors can occur due to the asynchronous nature of Pino. The fix to this is to allow for out-of-order logs in the Loki configuration. The reason why Loki doesn't have this enabled by default is because Promtail accounts for ordering constraints, however the same issue can also happen with promtail in high-load or when working with distributed networks.
271301

272302
## Dropped logs
273-
If any network issues occur, the logs can be dropped. The recommendation is therefore to implement a failover solution, this will vary greatly from system to system.
303+
304+
Logs can be dropped in two scenarios:
305+
306+
1. **Network issues**: If Loki is unreachable, logs in the current batch will be lost.
307+
2. **Buffer overflow**: When batching is enabled and the buffer reaches `maxBufferSize` (default: 10,000), the oldest logs are dropped to make room for new ones. This prevents memory exhaustion if Loki becomes unavailable for an extended period.
308+
309+
For critical applications, consider implementing a failover solution or adjusting `maxBufferSize` based on your memory constraints and acceptable data loss.
274310

275311
## Node v18+ Required
276312
As the pino-loki library uses the native Node fetch, any consumer must be using a version of Node greater than v18.0.0.

examples/batching.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ const transport = pino.transport<LokiOptions>({
1313
target: '../dist/index.mjs',
1414

1515
options: {
16-
batching: true,
17-
interval: 2,
16+
batching: { interval: 2 },
1817

1918
// These labels will be added to every log
2019
labels: { application: 'MY-APP' },

src/cli/args.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,23 @@ export const options = {
3434
default: '/loki/api/v1/push',
3535
help: 'Path to the Loki push API',
3636
},
37-
'batch': {
37+
'batching': {
3838
type: 'boolean',
3939
default: true,
4040
short: 'b',
4141
help: 'Should logs be sent in batch mode',
4242
},
43-
'interval': {
43+
'batching-interval': {
4444
type: 'string',
4545
default: '5',
4646
short: 'i',
4747
help: 'The interval at which batched logs are sent in seconds',
4848
},
49+
'batching-max-buffer-size': {
50+
type: 'string',
51+
default: '10000',
52+
help: 'Maximum number of logs to buffer (0 for unlimited)',
53+
},
4954
'timeout': { type: 'string', default: '2000', short: 't', help: 'Timeout for request to Loki' },
5055
'silenceErrors': {
5156
type: 'boolean',

src/cli/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,14 @@ export const createPinoLokiConfigFromArgs = () => {
5050
endpoint: values.endpoint,
5151
timeout: values.timeout ? Number(values.timeout) : undefined,
5252
silenceErrors: values.silenceErrors,
53-
batching: values.batch,
54-
interval: values.interval ? Number(values.interval) : undefined,
53+
batching: values.batching === false
54+
? false
55+
: {
56+
interval: values['batching-interval'] ? Number(values['batching-interval']) : undefined,
57+
maxBufferSize: values['batching-max-buffer-size']
58+
? Number(values['batching-max-buffer-size'])
59+
: undefined,
60+
},
5561
replaceTimestamp: values.replaceTimestamp,
5662
labels: values.labels ? JSON.parse(values.labels) : undefined,
5763
propsToLabels: propsLabels,

src/index.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,23 @@ import abstractTransportBuild from 'pino-abstract-transport'
33
import debug from './debug.ts'
44
import { LogPusher } from './log_pusher.ts'
55
import { LokiLogLevel } from './constants.ts'
6-
import type { PinoLog, LokiOptions } from './types.ts'
6+
import type { PinoLog, LokiOptions, BatchingOptions } from './types.ts'
7+
8+
interface ResolvedBatching {
9+
enabled: boolean
10+
interval: number
11+
maxBufferSize: number
12+
}
13+
14+
function resolveBatching(batching: LokiOptions['batching']): ResolvedBatching {
15+
if (batching === false) return { enabled: false, interval: 5, maxBufferSize: 10_000 }
16+
17+
return {
18+
enabled: true,
19+
interval: batching?.interval ?? 5,
20+
maxBufferSize: batching?.maxBufferSize ?? 10_000,
21+
}
22+
}
723

824
/**
925
* Resolves the options for the Pino Loki transport
@@ -14,8 +30,7 @@ function resolveOptions(options: LokiOptions) {
1430
endpoint: options.endpoint ?? 'loki/api/v1/push',
1531
timeout: options.timeout ?? 30_000,
1632
silenceErrors: options.silenceErrors ?? false,
17-
batching: options.batching ?? true,
18-
interval: options.interval ?? 5,
33+
batching: resolveBatching(options.batching),
1934
replaceTimestamp: options.replaceTimestamp ?? false,
2035
propsToLabels: options.propsToLabels ?? [],
2136
convertArrays: options.convertArrays ?? false,
@@ -36,21 +51,23 @@ function pinoLoki(userOptions: LokiOptions) {
3651

3752
return abstractTransportBuild(
3853
async (source) => {
39-
if (options.batching) {
54+
if (options.batching.enabled) {
4055
batchInterval = setInterval(async () => {
4156
debug(`Batch interval reached, sending ${pinoLogBuffer.length} logs to Loki`)
4257

43-
if (pinoLogBuffer.length === 0) {
44-
return
45-
}
58+
if (pinoLogBuffer.length === 0) return
4659

4760
logPusher.push(pinoLogBuffer)
4861
pinoLogBuffer = []
49-
}, options.interval! * 1000)
62+
}, options.batching.interval * 1000)
5063
}
5164

5265
for await (const obj of source) {
53-
if (options.batching) {
66+
if (options.batching.enabled) {
67+
if (options.batching.maxBufferSize > 0 && pinoLogBuffer.length >= options.batching.maxBufferSize) {
68+
const dropped = pinoLogBuffer.shift()
69+
debug(`[PinoLoki] Buffer full, dropping oldest log: ${JSON.stringify(dropped)}`)
70+
}
5471
pinoLogBuffer.push(obj)
5572
continue
5673
}
@@ -64,7 +81,7 @@ function pinoLoki(userOptions: LokiOptions) {
6481
* and clear the interval
6582
*/
6683
async close() {
67-
if (options.batching) {
84+
if (options.batching.enabled) {
6885
clearInterval(batchInterval!)
6986
await logPusher.push(pinoLogBuffer)
7087
}
@@ -74,4 +91,4 @@ function pinoLoki(userOptions: LokiOptions) {
7491
}
7592

7693
export default pinoLoki
77-
export { LokiLogLevel, pinoLoki, type LokiOptions, type PinoLog }
94+
export { LokiLogLevel, pinoLoki, type LokiOptions, type PinoLog, type BatchingOptions }

src/types.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,26 @@ export interface PinoLog {
2828
[key: string]: any
2929
}
3030

31+
/**
32+
* Batching configuration options
33+
*/
34+
export interface BatchingOptions {
35+
/**
36+
* The interval at which batched logs are sent in seconds
37+
*
38+
* @default 5
39+
*/
40+
interval?: number
41+
42+
/**
43+
* Maximum number of logs to buffer before dropping the oldest ones.
44+
* Set to 0 for unlimited buffer (not recommended).
45+
*
46+
* @default 10_000
47+
*/
48+
maxBufferSize?: number
49+
}
50+
3151
/**
3252
* Options for the Pino-Loki transport
3353
*/
@@ -59,18 +79,11 @@ export interface LokiOptions {
5979
silenceErrors?: boolean
6080

6181
/**
62-
* Should logs be sent in batch mode
63-
*
64-
* @default true
65-
*/
66-
batching?: boolean
67-
68-
/**
69-
* The interval at which batched logs are sent in seconds
82+
* Batching configuration. Set to `false` to disable batching entirely.
7083
*
71-
* @default 5
84+
* @default { enabled: true, interval: 5, maxBufferSize: 10_000 }
7285
*/
73-
interval?: number
86+
batching?: false | BatchingOptions
7487

7588
/**
7689
* Replace pino logs timestamps with Date.now()

tests/integration/loki.spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ test.group('Loki integration', () => {
8080
{ level: 'info' },
8181
pinoLoki({
8282
...credentials,
83-
batching: true,
84-
interval: 1,
83+
batching: { interval: 1 },
8584
labels: { application },
8685
}),
8786
)
@@ -107,8 +106,7 @@ test.group('Loki integration', () => {
107106
target: '../../dist/index.mjs',
108107
options: {
109108
...credentials,
110-
batching: true,
111-
interval: 10,
109+
batching: { interval: 10 },
112110
labels: { application },
113111
},
114112
})

0 commit comments

Comments
 (0)