@alex-michaud/pino-graylog-transport 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexandre Michaud
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,489 @@
1
+ # pino-graylog-transport
2
+
3
+ A Pino transport module that sends log messages to Graylog using the GELF (Graylog Extended Log Format) protocol over TCP, TLS, or UDP.
4
+
5
+ ## Features
6
+
7
+ - 🚀 Full support for Pino transports API
8
+ - 📦 GELF (Graylog Extended Log Format) message formatting
9
+ - 🔒 TLS, TCP, and UDP protocol support (TLS secure by default)
10
+ - 🔧 Configurable facility, host, and port
11
+ - 📊 Automatic log level conversion (Pino → Syslog)
12
+ - 🏷️ Custom field support with GELF underscore prefix
13
+ - ⚡ High-performance async message sending with buffering and reconnection logic
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @alex-michaud/pino-graylog-transport pino
19
+ ```
20
+
21
+ ## Publishing & Installation
22
+
23
+ This package is published under the scope `@alex-michaud`. To install, use the following command:
24
+
25
+ ```bash
26
+ npm install @alex-michaud/pino-graylog-transport
27
+ ```
28
+
29
+ If you encounter permissions issues, you may need to add `--access public` to the install command:
30
+
31
+ ```bash
32
+ npm install @alex-michaud/pino-graylog-transport --access public
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ```javascript
38
+ const pino = require('pino');
39
+ const transport = require('@alex-michaud/pino-graylog-transport');
40
+
41
+ const transportInstance = transport({
42
+ host: 'graylog.example.com',
43
+ port: 12201,
44
+ // TLS is the default for secure transmission over networks
45
+ // Use protocol: 'tcp' only for local development (localhost)
46
+ protocol: 'tls',
47
+ facility: 'my-app',
48
+ staticMeta: {
49
+ environment: 'production',
50
+ service: 'api',
51
+ version: '1.0.0'
52
+ }
53
+ });
54
+
55
+ const logger = pino(transportInstance);
56
+
57
+ logger.info('Hello Graylog!');
58
+ ```
59
+
60
+ ## Configuration Options
61
+
62
+ | Option | Type | Default | Description |
63
+ |--------|------|---------|-------------|
64
+ | `host` | string | `'localhost'` | Graylog server hostname |
65
+ | `port` | number | `12201` | Graylog GELF input port (standard GELF TCP port) |
66
+ | `protocol` | string | `'tls'` | Protocol to use (`'tcp'`, `'tls'`, or `'udp'`). **Default is `'tls'` for security** - uses encrypted connection to prevent exposure of sensitive data. Use `'tcp'` only for local development. Use `'udp'` for high-throughput scenarios where delivery guarantees are not required. |
67
+ | `facility` | string | `hostname` | **Application/service identifier** sent with every message. Used to categorize logs by application in Graylog (e.g., `'api-gateway'`, `'auth-service'`). Sent as `_facility` additional field per [GELF spec](https://go2docs.graylog.org/current/getting_in_log_data/gelf.html). |
68
+ | `hostname` | string | `os.hostname()` | Host field in GELF messages (the machine/server name) |
69
+ | `staticMeta` | object | `{}` | Static fields included in **every** log message (e.g., auth tokens, environment, datacenter). These are sent as GELF custom fields with underscore prefix. |
70
+ | `maxQueueSize` | number | `1000` | Max messages to queue when disconnected |
71
+ | `onError` | function | `console.error` | Custom error handler |
72
+ | `onReady` | function | `undefined` | Callback when connection is established |
73
+ | `autoConnect` | boolean | `true` | If `false`, don't establish connection on initialization. Only applies to TCP/TLS; UDP always initializes immediately since it's connectionless. |
74
+
75
+ ### ⚠️ Security Note
76
+
77
+ The default protocol is **`tls`** to ensure logs are transmitted securely over encrypted connections. This is important when:
78
+ - Sending logs over untrusted networks (internet, shared corporate networks)
79
+ - Including authentication tokens in `staticMeta` (e.g., `'X-OVH-TOKEN'`)
80
+ - Logging sensitive data (PII, API keys, internal URLs)
81
+
82
+ Only use `protocol: 'tcp'` for local development when Graylog is running on `localhost`.
83
+
84
+ ## Using with Authentication Tokens
85
+
86
+ Some Graylog services require authentication tokens to be sent with every log message. Use `staticMeta` to include these tokens and any other metadata that should be sent with **all** log messages:
87
+
88
+ ```javascript
89
+ const transport = require('@alex-michaud/pino-graylog-transport');
90
+
91
+ // Example: OVH Logs Data Platform
92
+ const stream = transport({
93
+ host: 'bhs1.logs.ovh.com',
94
+ port: 12202,
95
+ protocol: 'tls',
96
+ staticMeta: {
97
+ 'X-OVH-TOKEN': 'your-ovh-token-here'
98
+ }
99
+ });
100
+
101
+ // Example: Generic cloud provider with token
102
+ const stream = transport({
103
+ host: 'graylog.example.com',
104
+ port: 12201,
105
+ protocol: 'tls',
106
+ staticMeta: {
107
+ token: 'your-auth-token',
108
+ environment: 'production',
109
+ datacenter: 'us-east-1'
110
+ }
111
+ });
112
+ ```
113
+
114
+ All fields in `staticMeta` will be included in every GELF message with an underscore prefix (e.g., `_X-OVH-TOKEN`, `_token`, `_environment`).
115
+
116
+ ## Understanding Configuration Fields
117
+
118
+ ### `facility` vs `hostname` vs `staticMeta`
119
+
120
+ These three configuration options serve different purposes:
121
+
122
+ | Field | Purpose | Example | GELF Field | When to Use |
123
+ |-------|---------|---------|------------|-------------|
124
+ | `facility` | **Application/service identifier** | `'api-gateway'`, `'auth-service'` | `_facility` | Identify which application/microservice sent the log |
125
+ | `hostname` | **Machine/server identifier** | `'web-server-01'`, `'us-east-1a'` | `host` | Identify which machine/container/pod sent the log |
126
+ | `staticMeta` | **Context metadata** | `{ token: 'abc', env: 'prod' }` | `_token`, `_env` | Add authentication tokens or contextual info |
127
+
128
+ ### Example: Microservices Architecture
129
+
130
+ ```javascript
131
+ // API Gateway service running on server 1
132
+ transport({
133
+ facility: 'api-gateway', // What service?
134
+ hostname: 'web-server-01', // Which machine?
135
+ staticMeta: {
136
+ environment: 'production', // Extra context
137
+ region: 'us-east-1',
138
+ version: '2.1.0'
139
+ }
140
+ });
141
+
142
+ // Auth Service running on server 2
143
+ transport({
144
+ facility: 'auth-service', // What service?
145
+ hostname: 'web-server-02', // Which machine?
146
+ staticMeta: {
147
+ environment: 'production',
148
+ region: 'us-east-1',
149
+ version: '1.5.3'
150
+ }
151
+ });
152
+ ```
153
+
154
+ In Graylog, you can then:
155
+ - Filter by `_facility:api-gateway` to see all API gateway logs
156
+ - Filter by `host:web-server-01` to see all logs from that server
157
+ - Filter by `_environment:production` to see production logs across all services
158
+
159
+ ## Local Development with Docker
160
+
161
+ This repository includes a Docker Compose configuration to run Graylog locally for testing.
162
+
163
+ ### Start Graylog
164
+
165
+ ```bash
166
+ npm run docker:up
167
+ # or
168
+ docker compose up -d
169
+ ```
170
+
171
+ Wait about 30 seconds for Graylog to fully start, then run the setup script to create the GELF inputs:
172
+
173
+ ```bash
174
+ npm run docker:setup
175
+ ```
176
+
177
+ Then access the web interface at:
178
+ - URL: http://localhost:9005
179
+ - Username: `admin`
180
+ - Password: `admin`
181
+
182
+ ### Stop Graylog
183
+
184
+ ```bash
185
+ npm run docker:down
186
+ # or
187
+ docker compose down
188
+ ```
189
+
190
+ ## Usage Examples
191
+
192
+ ### Basic Logging
193
+
194
+ ```javascript
195
+ const pino = require('pino');
196
+ const transport = require('@alex-michaud/pino-graylog-transport');
197
+
198
+ // For local development (localhost)
199
+ const logger = pino(transport({
200
+ host: 'localhost',
201
+ port: 12201,
202
+ protocol: 'tcp' // Safe to use TCP for localhost
203
+ }));
204
+
205
+ logger.info('Application started');
206
+ logger.warn('Warning message');
207
+ logger.error('Error message');
208
+ ```
209
+
210
+ ### Logging with Custom Fields
211
+
212
+ ```javascript
213
+ logger.info({
214
+ userId: 123,
215
+ action: 'login',
216
+ ip: '192.168.1.1'
217
+ }, 'User logged in');
218
+
219
+ // In Graylog, these will appear as: _userId, _action, _ip
220
+ ```
221
+
222
+ ### Error Logging with Stack Traces
223
+
224
+ ```javascript
225
+ try {
226
+ throw new Error('Something went wrong!');
227
+ } catch (err) {
228
+ logger.error({ err }, 'An error occurred');
229
+ // Stack trace will be sent as full_message in GELF
230
+ }
231
+ ```
232
+
233
+ ### TCP Protocol
234
+
235
+ ```javascript
236
+ const logger = pino(await transport({
237
+ host: 'localhost',
238
+ port: 12201,
239
+ protocol: 'tcp' // Use TCP instead of TLS
240
+ }));
241
+ ```
242
+
243
+ ### UDP Protocol
244
+
245
+ ```javascript
246
+ const logger = pino(await transport({
247
+ host: 'localhost',
248
+ port: 12201,
249
+ protocol: 'udp' // Use UDP for high-throughput, fire-and-forget logging
250
+ }));
251
+ ```
252
+
253
+ **Note:** UDP is connectionless and does not guarantee message delivery. Use it when:
254
+ - You need maximum throughput
255
+ - Occasional message loss is acceptable
256
+ - You're logging to a local Graylog instance
257
+ - Network reliability is high
258
+
259
+ **UDP Limitations:**
260
+ - Messages are limited to **8KB (8192 bytes)** per GELF UDP specification
261
+ - Messages exceeding this size are **rejected** (not sent)
262
+ - For large messages, use TCP or TLS protocols instead
263
+ - No delivery guarantees (fire-and-forget)
264
+
265
+ **Bun Runtime:** When running under [Bun](https://bun.sh/), the UDP transport automatically uses Bun's native `Bun.udpSocket()` API for better performance. Falls back to Node's `dgram` module if unavailable.
266
+
267
+ ### Run the Example
268
+
269
+ ```bash
270
+ # Start Graylog first
271
+ npm run docker:up
272
+
273
+ # Run the example (after installing dependencies)
274
+ npm install
275
+ node examples/basic.js
276
+
277
+ # View logs at http://localhost:9005
278
+ ```
279
+
280
+ ## Testing
281
+
282
+ ### Install Dependencies
283
+
284
+ ```bash
285
+ npm install
286
+ ```
287
+
288
+ ### Unit Tests
289
+
290
+ Run the library functionality tests (no external dependencies required):
291
+
292
+ ```bash
293
+ npm test
294
+ # or
295
+ npm run test:unit
296
+ ```
297
+
298
+ ### Integration Tests
299
+
300
+ Integration tests require a running Graylog instance:
301
+
302
+ ```bash
303
+ # Start Graylog first
304
+ npm run docker:up
305
+ npm run docker:setup
306
+
307
+ # Run integration tests
308
+ npm run test:integration
309
+ ```
310
+
311
+ ### Run All Tests
312
+
313
+ Run both unit and integration tests:
314
+
315
+ ```bash
316
+ npm run test:all
317
+ ```
318
+
319
+ ### Load Tests with k6
320
+
321
+ Load tests use [k6](https://k6.io) to simulate high-volume logging scenarios.
322
+
323
+ #### Install k6
324
+
325
+ ```bash
326
+ # macOS
327
+ brew install k6
328
+ ```
329
+ ```bash
330
+ # Linux
331
+ sudo gpg -k
332
+ sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
333
+ echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
334
+ sudo apt-get update
335
+ sudo apt-get install k6
336
+ ```
337
+
338
+ ```bash
339
+ # Windows
340
+ choco install k6
341
+ ```
342
+
343
+ #### Run Load Tests
344
+
345
+ ```bash
346
+ # Start Graylog
347
+ npm run docker:up
348
+
349
+ # Setup Graylog inputs (wait for Graylog to be ready first)
350
+ npm run docker:setup
351
+
352
+ # Start the load test server
353
+ npm run start:load-server
354
+
355
+ # In another terminal, run the k6 load test
356
+ npm run test:load
357
+
358
+ # Or run a quick smoke test (20 seconds)
359
+ npm run test:smoke
360
+ ```
361
+
362
+ The load test will:
363
+ - Ramp up from 0 to 50 virtual users
364
+ - Send thousands of log messages
365
+ - Measure throughput and latency
366
+ - Verify 95% success rate
367
+
368
+ ## Project Structure
369
+
370
+ ```
371
+ pino-graylog-transport/
372
+ ├── lib/ # Source code
373
+ │ ├── index.ts # Main transport entry point
374
+ │ ├── gelf-formatter.ts # GELF message formatter
375
+ │ └── graylog-transport.ts # TCP/TLS transport
376
+ ├── test/ # Tests
377
+ │ ├── unit/ # Unit tests
378
+ │ │ ├── gelf-formatter.test.ts
379
+ │ │ ├── graylog-transport.test.ts
380
+ │ │ └── integration.test.ts
381
+ │ ├── load/ # Load tests
382
+ │ │ ├── load-test.ts # k6 load test script
383
+ │ │ ├── smoke-test.ts # k6 smoke test script
384
+ │ │ └── server.ts # Test server for load testing
385
+ │ └── benchmark/ # Performance benchmarks
386
+ │ ├── benchmark.ts # Microbenchmark (formatting)
387
+ │ ├── comparison-server.ts # Server for pino vs winston test
388
+ │ └── comparison-load-test.ts # k6 comparison load test
389
+ ├── examples/ # Usage examples
390
+ │ └── basic.js
391
+ ├── docker-compose.yml # Graylog local setup
392
+ └── package.json
393
+ ```
394
+
395
+ ## Log Level Mapping
396
+
397
+ Pino log levels are automatically converted to Syslog severity levels for Graylog:
398
+
399
+ | Pino Level | Numeric | Syslog Level | Numeric |
400
+ |------------|---------|--------------|---------|
401
+ | fatal | 60 | Critical | 2 |
402
+ | error | 50 | Error | 3 |
403
+ | warn | 40 | Warning | 4 |
404
+ | info | 30 | Informational| 6 |
405
+ | debug | 20 | Debug | 7 |
406
+ | trace | 10 | Debug | 7 |
407
+
408
+ ## GELF Message Format
409
+
410
+ The transport converts Pino log objects to GELF format:
411
+
412
+ - `short_message`: The log message
413
+ - `full_message`: Stack trace (if present)
414
+ - `level`: Syslog severity level
415
+ - `timestamp`: Unix timestamp
416
+ - `host`: Hostname
417
+ - `facility`: Application/service name
418
+ - `_*`: Custom fields (all Pino log object properties)
419
+
420
+ ## Performance
421
+
422
+ The GELF formatting logic is optimized for speed. Benchmarks were run using [mitata](https://github.com/evanwashere/mitata) to measure the overhead of message transformation (excluding network I/O).
423
+
424
+ ### Benchmark Results
425
+
426
+ | Benchmark | Time | Description |
427
+ |-----------|------|-------------|
428
+ | JSON.stringify (Raw) | 614 ns | Baseline - just serialization, no transformation |
429
+ | **pino-graylog-transport** | **1.87 µs** | Our GELF formatter |
430
+ | Manual GELF Construction | 2.21 µs | Simulated naive implementation |
431
+
432
+ ### Key Takeaways
433
+
434
+ - ✅ **18% faster** than a naive manual GELF construction approach
435
+ - ✅ **~535,000 messages/second** theoretical formatting throughput (single-threaded)
436
+ - ✅ **Negligible overhead**: The ~1.25 µs formatting overhead is 500-50,000x smaller than typical network latency
437
+
438
+ ### Run Benchmarks
439
+
440
+ ```bash
441
+ # Run formatting microbenchmark (no network)
442
+ npm run benchmark
443
+ ```
444
+
445
+ ### Comparison Load Test (pino vs winston)
446
+
447
+ This test compares the real-world performance of `@alex-michaud/pino-graylog-transport` against `winston + winston-log2gelf`:
448
+
449
+ ```bash
450
+ # Start Graylog
451
+ npm run docker:up
452
+ npm run docker:setup
453
+
454
+ # Start the comparison server (in one terminal)
455
+ npm run start:comparison-server
456
+
457
+ # Run the comparison load test (in another terminal)
458
+ npm run benchmark:load
459
+ ```
460
+
461
+ The test runs three scenarios in parallel:
462
+ - **Baseline**: No logging (measures pure HTTP overhead)
463
+ - **Pino**: Using @alex-michaud/pino-graylog-transport
464
+ - **Winston**: Using winston + winston-log2gelf
465
+
466
+ Compare the `*_duration` metrics to see the logging overhead for each library.
467
+
468
+ ## License
469
+
470
+ MIT — see the [LICENSE](./LICENSE) file for details.
471
+
472
+ ## Contributing
473
+
474
+ Contributions are welcome! Please feel free to submit a Pull Request.
475
+
476
+ ## Requirements
477
+
478
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org/)
479
+
480
+ This package requires Node.js >= 22 (declared in package.json `engines`). The CI and release workflows prefer the latest Node LTS. If your local Node version is older, upgrade Node (for example, using nvm):
481
+
482
+ ```bash
483
+ # Install nvm (if not present)
484
+ # https://github.com/nvm-sh/nvm#installing-and-updating
485
+
486
+ # Use latest LTS
487
+ nvm install --lts
488
+ nvm use --lts
489
+ ```
@@ -0,0 +1,11 @@
1
+ export interface GelfMessage {
2
+ version: '1.1';
3
+ host: string;
4
+ short_message: string;
5
+ full_message?: string;
6
+ timestamp: number;
7
+ level: number;
8
+ [key: string]: unknown;
9
+ }
10
+ export declare const mapPinoLevelToGelf: (pinoLevel: number | undefined) => number;
11
+ export declare function formatGelfMessage(chunk: unknown, hostname: string, facility: string, staticMeta?: Record<string, unknown>): string;
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mapPinoLevelToGelf = void 0;
4
+ exports.formatGelfMessage = formatGelfMessage;
5
+ // OPTIMIZATION: Hoist constant outside the hot path and use Set for O(1) lookup
6
+ const EXCLUDED_FIELDS = new Set([
7
+ 'msg',
8
+ 'message',
9
+ 'level',
10
+ 'time',
11
+ 'pid',
12
+ 'hostname',
13
+ 'stack',
14
+ 'v',
15
+ 'err',
16
+ ]);
17
+ const mapPinoLevelToGelf = (pinoLevel) => {
18
+ // Pino levels: 10 trace, 20 debug, 30 info, 40 warn, 50 error, 60 fatal
19
+ // GELF levels: 0 emergency, 1 alert, 2 critical, 3 error, 4 warning, 5 notice, 6 info, 7 debug
20
+ if (!pinoLevel)
21
+ return 6; // default to info
22
+ if (pinoLevel >= 60)
23
+ return 2; // fatal -> critical
24
+ if (pinoLevel >= 50)
25
+ return 3; // error
26
+ if (pinoLevel >= 40)
27
+ return 4; // warning
28
+ if (pinoLevel >= 30)
29
+ return 6; // info
30
+ if (pinoLevel >= 20)
31
+ return 7; // debug
32
+ return 7; // trace -> debug
33
+ };
34
+ exports.mapPinoLevelToGelf = mapPinoLevelToGelf;
35
+ function parseChunk(chunk) {
36
+ // OPTIMIZATION: Check object type first (most common case from Pino)
37
+ if (typeof chunk === 'object' && chunk !== null) {
38
+ return chunk;
39
+ }
40
+ // Handle strings and buffers
41
+ let str = '';
42
+ if (typeof chunk === 'string') {
43
+ str = chunk;
44
+ }
45
+ else if (Buffer.isBuffer(chunk)) {
46
+ str = chunk.toString();
47
+ }
48
+ // OPTIMIZATION: Heuristic check - only try parsing if it looks like JSON
49
+ // This avoids expensive try-catch throws on plain text logs
50
+ const firstChar = str.trim()[0];
51
+ if (firstChar === '{' || firstChar === '[') {
52
+ try {
53
+ return JSON.parse(str);
54
+ }
55
+ catch (_a) {
56
+ // If parse fails despite looking like JSON, fall back to wrapping it
57
+ }
58
+ }
59
+ return { msg: str };
60
+ }
61
+ function extractMessage(obj) {
62
+ // OPTIMIZATION: Use explicit type checks (slightly faster than truthiness)
63
+ if (typeof obj.msg === 'string')
64
+ return obj.msg;
65
+ if (typeof obj.message === 'string')
66
+ return obj.message;
67
+ if (typeof obj.short_message === 'string')
68
+ return obj.short_message;
69
+ return JSON.stringify(obj);
70
+ }
71
+ function addStaticMetadata(gelfMessage, staticMeta) {
72
+ // OPTIMIZATION: Use for...in to avoid allocating Object.entries array
73
+ for (const key in staticMeta) {
74
+ const value = staticMeta[key];
75
+ if (value !== undefined && value !== null) {
76
+ // OPTIMIZATION: Use charCodeAt for underscore check (95 is '_')
77
+ const fieldName = key.charCodeAt(0) === 95 ? key : `_${key}`;
78
+ gelfMessage[fieldName] =
79
+ typeof value === 'object' ? JSON.stringify(value) : value;
80
+ }
81
+ }
82
+ }
83
+ function addStackTrace(gelfMessage, obj) {
84
+ // Check for direct stack
85
+ if (typeof obj.stack === 'string') {
86
+ gelfMessage.full_message = obj.stack;
87
+ return;
88
+ }
89
+ // Check for nested err.stack using optional chaining
90
+ const err = obj.err;
91
+ if (typeof (err === null || err === void 0 ? void 0 : err.stack) === 'string') {
92
+ gelfMessage.full_message = err.stack;
93
+ }
94
+ }
95
+ function addCustomFields(gelfMessage, obj) {
96
+ // OPTIMIZATION: Use for...in loop instead of Object.entries
97
+ for (const key in obj) {
98
+ // OPTIMIZATION: O(1) lookup in Set instead of Array.includes
99
+ if (EXCLUDED_FIELDS.has(key))
100
+ continue;
101
+ const value = obj[key];
102
+ if (value === undefined || value === null)
103
+ continue;
104
+ // OPTIMIZATION: Use charCodeAt for underscore check
105
+ const fieldName = key.charCodeAt(0) === 95 ? key : `_${key}`;
106
+ // Avoid overwriting standard fields or static meta
107
+ if (fieldName in gelfMessage)
108
+ continue;
109
+ // GELF doesn't support nested objects well, stringify them
110
+ gelfMessage[fieldName] =
111
+ typeof value === 'object' ? JSON.stringify(value) : value;
112
+ }
113
+ // Add process info
114
+ if (obj.pid) {
115
+ gelfMessage._pid = obj.pid;
116
+ }
117
+ }
118
+ function formatGelfMessage(chunk, hostname, facility, staticMeta = {}) {
119
+ const obj = parseChunk(chunk);
120
+ // Pino adds 'time' field automatically (milliseconds since epoch)
121
+ // We preserve it to maintain accurate event timing even if messages are queued
122
+ // Fallback to current time for non-Pino messages
123
+ // OPTIMIZATION: Use typeof check and multiplication (faster than division)
124
+ const timestamp = typeof obj.time === 'number' ? obj.time * 1e-3 : Date.now() * 1e-3;
125
+ // Build GELF 1.1 message
126
+ const gelfMessage = {
127
+ version: '1.1',
128
+ host: hostname,
129
+ short_message: extractMessage(obj),
130
+ timestamp,
131
+ level: (0, exports.mapPinoLevelToGelf)(obj.level),
132
+ };
133
+ // Add facility as additional field (deprecated in GELF spec, now sent as custom field)
134
+ if (facility) {
135
+ gelfMessage._facility = facility;
136
+ }
137
+ addStaticMetadata(gelfMessage, staticMeta);
138
+ addStackTrace(gelfMessage, obj);
139
+ addCustomFields(gelfMessage, obj);
140
+ return JSON.stringify(gelfMessage);
141
+ }