@fabbahiense/pulsar-pino-transport 0.1.0
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/README.md +67 -0
- package/dist/index.cjs +150 -0
- package/dist/index.d.cts +50 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +115 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @pulsar/pino-transport
|
|
2
|
+
|
|
3
|
+
Pino transport for [Pulsar](https://github.com/your-org/pulsar) — send your application logs to Pulsar in real-time.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @pulsar/pino-transport
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import pino from 'pino'
|
|
15
|
+
|
|
16
|
+
const logger = pino({
|
|
17
|
+
transport: {
|
|
18
|
+
target: '@pulsar/pino-transport',
|
|
19
|
+
options: {
|
|
20
|
+
url: 'https://your-pulsar-instance.com',
|
|
21
|
+
apiKey: 'psk_your_api_key',
|
|
22
|
+
source: 'my-api', // optional, default: 'default'
|
|
23
|
+
batchSize: 50, // optional, default: 50
|
|
24
|
+
flushInterval: 2000, // optional, default: 2000ms
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
logger.info('Server started')
|
|
30
|
+
logger.error({ traceId: 'abc-123' }, 'Something went wrong')
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Batched log delivery (configurable batch size)
|
|
36
|
+
- Automatic retry with exponential backoff (3 attempts)
|
|
37
|
+
- In-memory queue — no logs lost on temporary network failures
|
|
38
|
+
- Zero dependencies (uses native `fetch`)
|
|
39
|
+
- TypeScript types included
|
|
40
|
+
- Dual CJS/ESM package
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
| Option | Type | Default | Description |
|
|
45
|
+
|--------|------|---------|-------------|
|
|
46
|
+
| `url` | `string` | — | Pulsar server URL (required) |
|
|
47
|
+
| `apiKey` | `string` | — | API key for authentication (required) |
|
|
48
|
+
| `source` | `string` | `'default'` | Service name in Pulsar |
|
|
49
|
+
| `batchSize` | `number` | `50` | Logs buffered before flush |
|
|
50
|
+
| `flushInterval` | `number` | `2000` | Flush interval in ms |
|
|
51
|
+
|
|
52
|
+
## Trace ID
|
|
53
|
+
|
|
54
|
+
The transport automatically picks up trace IDs from these fields:
|
|
55
|
+
|
|
56
|
+
- `traceId`
|
|
57
|
+
- `trace_id`
|
|
58
|
+
- `requestId`
|
|
59
|
+
- `req.id`
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
logger.info({ traceId: 'my-trace-id' }, 'Processing request')
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
createPulsarTransport: () => createPulsarTransport,
|
|
34
|
+
default: () => index_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
var LEVEL_MAP = {
|
|
38
|
+
10: "trace",
|
|
39
|
+
20: "debug",
|
|
40
|
+
30: "info",
|
|
41
|
+
40: "warn",
|
|
42
|
+
50: "error",
|
|
43
|
+
60: "fatal"
|
|
44
|
+
};
|
|
45
|
+
var MAX_RETRIES = 3;
|
|
46
|
+
var RETRY_BASE_MS = 1e3;
|
|
47
|
+
function parseTimestamp(time) {
|
|
48
|
+
if (!time) return Date.now();
|
|
49
|
+
if (typeof time === "number") return time;
|
|
50
|
+
if (/^\d{13}$/.test(time)) return parseInt(time, 10);
|
|
51
|
+
const parsed = new Date(time).getTime();
|
|
52
|
+
return isNaN(parsed) ? Date.now() : parsed;
|
|
53
|
+
}
|
|
54
|
+
async function flushWithRetry(url, apiKey, batch, retryQueue) {
|
|
55
|
+
const headers = {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"Authorization": `Bearer ${apiKey}`
|
|
58
|
+
};
|
|
59
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch(`${url}/api/ingest/batch`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers,
|
|
64
|
+
body: JSON.stringify({ logs: batch })
|
|
65
|
+
});
|
|
66
|
+
if (res.ok) return;
|
|
67
|
+
if (res.status >= 400 && res.status < 500) return;
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
71
|
+
const delay = RETRY_BASE_MS * Math.pow(2, attempt);
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
retryQueue.push(...batch);
|
|
76
|
+
}
|
|
77
|
+
function createPulsarTransport(options) {
|
|
78
|
+
const {
|
|
79
|
+
url,
|
|
80
|
+
apiKey,
|
|
81
|
+
source,
|
|
82
|
+
serviceName,
|
|
83
|
+
batchSize = 50,
|
|
84
|
+
flushInterval = 2e3
|
|
85
|
+
} = options;
|
|
86
|
+
const resolvedSource = source || serviceName || "default";
|
|
87
|
+
return {
|
|
88
|
+
transport: {
|
|
89
|
+
target: "@pulsar/pino-transport",
|
|
90
|
+
options: { url, apiKey, source: resolvedSource, batchSize, flushInterval }
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function index_default(options) {
|
|
95
|
+
const {
|
|
96
|
+
url = "http://localhost:4000",
|
|
97
|
+
apiKey = "",
|
|
98
|
+
source,
|
|
99
|
+
serviceName,
|
|
100
|
+
batchSize = 50,
|
|
101
|
+
flushInterval = 2e3
|
|
102
|
+
} = options;
|
|
103
|
+
const resolvedSource = source || serviceName || "default";
|
|
104
|
+
const { default: build } = await import("pino-abstract-transport");
|
|
105
|
+
const buffer = [];
|
|
106
|
+
const retryQueue = [];
|
|
107
|
+
let timer = null;
|
|
108
|
+
async function flush() {
|
|
109
|
+
if (retryQueue.length > 0) {
|
|
110
|
+
buffer.unshift(...retryQueue.splice(0));
|
|
111
|
+
}
|
|
112
|
+
if (buffer.length === 0) return;
|
|
113
|
+
const batch = buffer.splice(0, batchSize);
|
|
114
|
+
await flushWithRetry(url, apiKey, batch, retryQueue);
|
|
115
|
+
}
|
|
116
|
+
return build(async function(stream) {
|
|
117
|
+
for await (const obj of stream) {
|
|
118
|
+
buffer.push({
|
|
119
|
+
level: LEVEL_MAP[obj.level] || "info",
|
|
120
|
+
msg: obj.msg,
|
|
121
|
+
timestamp: parseTimestamp(obj.time),
|
|
122
|
+
serviceName: resolvedSource,
|
|
123
|
+
traceId: obj.traceId || obj.trace_id || obj.requestId || obj.req?.id || null,
|
|
124
|
+
method: obj.req?.method,
|
|
125
|
+
url: obj.req?.url,
|
|
126
|
+
statusCode: obj.res?.statusCode,
|
|
127
|
+
responseTime: obj.responseTime,
|
|
128
|
+
errorMessage: obj.err?.message,
|
|
129
|
+
errorStack: obj.err?.stack,
|
|
130
|
+
metadata: obj.metadata || null
|
|
131
|
+
});
|
|
132
|
+
if (buffer.length >= batchSize) {
|
|
133
|
+
await flush();
|
|
134
|
+
}
|
|
135
|
+
if (!timer) {
|
|
136
|
+
timer = setInterval(() => flush(), flushInterval);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (timer) clearInterval(timer);
|
|
140
|
+
await flush();
|
|
141
|
+
if (retryQueue.length > 0) {
|
|
142
|
+
buffer.push(...retryQueue.splice(0));
|
|
143
|
+
await flush();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
148
|
+
0 && (module.exports = {
|
|
149
|
+
createPulsarTransport
|
|
150
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as pino_abstract_transport from 'pino-abstract-transport';
|
|
2
|
+
import * as node_stream from 'node:stream';
|
|
3
|
+
|
|
4
|
+
interface PulsarTransportOptions {
|
|
5
|
+
/** Pulsar server URL (e.g., 'https://pulsar.example.com') */
|
|
6
|
+
url: string;
|
|
7
|
+
/** API key for authentication (psk_...) */
|
|
8
|
+
apiKey: string;
|
|
9
|
+
/** Service name that will appear in Pulsar (default: 'default') */
|
|
10
|
+
source?: string;
|
|
11
|
+
/** Alias for source */
|
|
12
|
+
serviceName?: string;
|
|
13
|
+
/** Number of logs to buffer before flushing (default: 50) */
|
|
14
|
+
batchSize?: number;
|
|
15
|
+
/** Flush interval in milliseconds (default: 2000) */
|
|
16
|
+
flushInterval?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates a Pino transport that sends logs to a Pulsar server.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import pino from 'pino'
|
|
24
|
+
* import { createPulsarTransport } from '@pulsar/pino-transport'
|
|
25
|
+
*
|
|
26
|
+
* const logger = pino(
|
|
27
|
+
* createPulsarTransport({
|
|
28
|
+
* url: 'https://pulsar.example.com',
|
|
29
|
+
* apiKey: 'psk_...',
|
|
30
|
+
* })
|
|
31
|
+
* )
|
|
32
|
+
*
|
|
33
|
+
* logger.info('Server started')
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
declare function createPulsarTransport(options: PulsarTransportOptions): {
|
|
37
|
+
transport: {
|
|
38
|
+
target: string;
|
|
39
|
+
options: {
|
|
40
|
+
url: string;
|
|
41
|
+
apiKey: string;
|
|
42
|
+
source: string;
|
|
43
|
+
batchSize: number;
|
|
44
|
+
flushInterval: number;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
declare function export_default(options: PulsarTransportOptions): Promise<node_stream.Transform & pino_abstract_transport.OnUnknown>;
|
|
49
|
+
|
|
50
|
+
export { type PulsarTransportOptions, createPulsarTransport, export_default as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as pino_abstract_transport from 'pino-abstract-transport';
|
|
2
|
+
import * as node_stream from 'node:stream';
|
|
3
|
+
|
|
4
|
+
interface PulsarTransportOptions {
|
|
5
|
+
/** Pulsar server URL (e.g., 'https://pulsar.example.com') */
|
|
6
|
+
url: string;
|
|
7
|
+
/** API key for authentication (psk_...) */
|
|
8
|
+
apiKey: string;
|
|
9
|
+
/** Service name that will appear in Pulsar (default: 'default') */
|
|
10
|
+
source?: string;
|
|
11
|
+
/** Alias for source */
|
|
12
|
+
serviceName?: string;
|
|
13
|
+
/** Number of logs to buffer before flushing (default: 50) */
|
|
14
|
+
batchSize?: number;
|
|
15
|
+
/** Flush interval in milliseconds (default: 2000) */
|
|
16
|
+
flushInterval?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Creates a Pino transport that sends logs to a Pulsar server.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import pino from 'pino'
|
|
24
|
+
* import { createPulsarTransport } from '@pulsar/pino-transport'
|
|
25
|
+
*
|
|
26
|
+
* const logger = pino(
|
|
27
|
+
* createPulsarTransport({
|
|
28
|
+
* url: 'https://pulsar.example.com',
|
|
29
|
+
* apiKey: 'psk_...',
|
|
30
|
+
* })
|
|
31
|
+
* )
|
|
32
|
+
*
|
|
33
|
+
* logger.info('Server started')
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
declare function createPulsarTransport(options: PulsarTransportOptions): {
|
|
37
|
+
transport: {
|
|
38
|
+
target: string;
|
|
39
|
+
options: {
|
|
40
|
+
url: string;
|
|
41
|
+
apiKey: string;
|
|
42
|
+
source: string;
|
|
43
|
+
batchSize: number;
|
|
44
|
+
flushInterval: number;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
declare function export_default(options: PulsarTransportOptions): Promise<node_stream.Transform & pino_abstract_transport.OnUnknown>;
|
|
49
|
+
|
|
50
|
+
export { type PulsarTransportOptions, createPulsarTransport, export_default as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var LEVEL_MAP = {
|
|
3
|
+
10: "trace",
|
|
4
|
+
20: "debug",
|
|
5
|
+
30: "info",
|
|
6
|
+
40: "warn",
|
|
7
|
+
50: "error",
|
|
8
|
+
60: "fatal"
|
|
9
|
+
};
|
|
10
|
+
var MAX_RETRIES = 3;
|
|
11
|
+
var RETRY_BASE_MS = 1e3;
|
|
12
|
+
function parseTimestamp(time) {
|
|
13
|
+
if (!time) return Date.now();
|
|
14
|
+
if (typeof time === "number") return time;
|
|
15
|
+
if (/^\d{13}$/.test(time)) return parseInt(time, 10);
|
|
16
|
+
const parsed = new Date(time).getTime();
|
|
17
|
+
return isNaN(parsed) ? Date.now() : parsed;
|
|
18
|
+
}
|
|
19
|
+
async function flushWithRetry(url, apiKey, batch, retryQueue) {
|
|
20
|
+
const headers = {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
"Authorization": `Bearer ${apiKey}`
|
|
23
|
+
};
|
|
24
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(`${url}/api/ingest/batch`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers,
|
|
29
|
+
body: JSON.stringify({ logs: batch })
|
|
30
|
+
});
|
|
31
|
+
if (res.ok) return;
|
|
32
|
+
if (res.status >= 400 && res.status < 500) return;
|
|
33
|
+
} catch {
|
|
34
|
+
}
|
|
35
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
36
|
+
const delay = RETRY_BASE_MS * Math.pow(2, attempt);
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
retryQueue.push(...batch);
|
|
41
|
+
}
|
|
42
|
+
function createPulsarTransport(options) {
|
|
43
|
+
const {
|
|
44
|
+
url,
|
|
45
|
+
apiKey,
|
|
46
|
+
source,
|
|
47
|
+
serviceName,
|
|
48
|
+
batchSize = 50,
|
|
49
|
+
flushInterval = 2e3
|
|
50
|
+
} = options;
|
|
51
|
+
const resolvedSource = source || serviceName || "default";
|
|
52
|
+
return {
|
|
53
|
+
transport: {
|
|
54
|
+
target: "@pulsar/pino-transport",
|
|
55
|
+
options: { url, apiKey, source: resolvedSource, batchSize, flushInterval }
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function index_default(options) {
|
|
60
|
+
const {
|
|
61
|
+
url = "http://localhost:4000",
|
|
62
|
+
apiKey = "",
|
|
63
|
+
source,
|
|
64
|
+
serviceName,
|
|
65
|
+
batchSize = 50,
|
|
66
|
+
flushInterval = 2e3
|
|
67
|
+
} = options;
|
|
68
|
+
const resolvedSource = source || serviceName || "default";
|
|
69
|
+
const { default: build } = await import("pino-abstract-transport");
|
|
70
|
+
const buffer = [];
|
|
71
|
+
const retryQueue = [];
|
|
72
|
+
let timer = null;
|
|
73
|
+
async function flush() {
|
|
74
|
+
if (retryQueue.length > 0) {
|
|
75
|
+
buffer.unshift(...retryQueue.splice(0));
|
|
76
|
+
}
|
|
77
|
+
if (buffer.length === 0) return;
|
|
78
|
+
const batch = buffer.splice(0, batchSize);
|
|
79
|
+
await flushWithRetry(url, apiKey, batch, retryQueue);
|
|
80
|
+
}
|
|
81
|
+
return build(async function(stream) {
|
|
82
|
+
for await (const obj of stream) {
|
|
83
|
+
buffer.push({
|
|
84
|
+
level: LEVEL_MAP[obj.level] || "info",
|
|
85
|
+
msg: obj.msg,
|
|
86
|
+
timestamp: parseTimestamp(obj.time),
|
|
87
|
+
serviceName: resolvedSource,
|
|
88
|
+
traceId: obj.traceId || obj.trace_id || obj.requestId || obj.req?.id || null,
|
|
89
|
+
method: obj.req?.method,
|
|
90
|
+
url: obj.req?.url,
|
|
91
|
+
statusCode: obj.res?.statusCode,
|
|
92
|
+
responseTime: obj.responseTime,
|
|
93
|
+
errorMessage: obj.err?.message,
|
|
94
|
+
errorStack: obj.err?.stack,
|
|
95
|
+
metadata: obj.metadata || null
|
|
96
|
+
});
|
|
97
|
+
if (buffer.length >= batchSize) {
|
|
98
|
+
await flush();
|
|
99
|
+
}
|
|
100
|
+
if (!timer) {
|
|
101
|
+
timer = setInterval(() => flush(), flushInterval);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (timer) clearInterval(timer);
|
|
105
|
+
await flush();
|
|
106
|
+
if (retryQueue.length > 0) {
|
|
107
|
+
buffer.push(...retryQueue.splice(0));
|
|
108
|
+
await flush();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
createPulsarTransport,
|
|
114
|
+
index_default as default
|
|
115
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fabbahiense/pulsar-pino-transport",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pino transport for Pulsar observability platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist"],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"pino-abstract-transport": ">=2.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"pino-abstract-transport": "^3.0.0",
|
|
26
|
+
"tsup": "^8.0.0",
|
|
27
|
+
"typescript": "^5.9.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": ["pino", "transport", "pulsar", "observability", "logging"],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|