apitally 1.0.0-beta.4 → 1.0.0-beta.5
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 +121 -98
- package/dist/activation.cjs +1 -1
- package/dist/activation.js +1 -1
- package/dist/config.cjs +6 -3
- package/dist/config.cjs.map +1 -1
- package/dist/config.js +6 -3
- package/dist/config.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/metrics.cjs +0 -3
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.js +0 -3
- package/dist/metrics.js.map +1 -1
- package/dist/providers.cjs +10 -3
- package/dist/providers.cjs.map +1 -1
- package/dist/providers.js +10 -3
- package/dist/providers.js.map +1 -1
- package/dist/redaction.cjs +9 -5
- package/dist/redaction.cjs.map +1 -1
- package/dist/redaction.js +9 -5
- package/dist/redaction.js.map +1 -1
- package/dist/requestObservation.cjs +22 -17
- package/dist/requestObservation.cjs.map +1 -1
- package/dist/requestObservation.js +22 -17
- package/dist/requestObservation.js.map +1 -1
- package/dist/spanExporter.cjs +10 -7
- package/dist/spanExporter.cjs.map +1 -1
- package/dist/spanExporter.js +10 -7
- package/dist/spanExporter.js.map +1 -1
- package/dist/spanProcessor.cjs +9 -20
- package/dist/spanProcessor.cjs.map +1 -1
- package/dist/spanProcessor.d.cts +2 -1
- package/dist/spanProcessor.d.ts +2 -1
- package/dist/spanProcessor.js +8 -21
- package/dist/spanProcessor.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ The SDK is an [OpenTelemetry](https://opentelemetry.io) distribution and works a
|
|
|
33
33
|
Learn more about Apitally on our 🌎 [website](https://apitally.io) or check out the 📚 [documentation](https://docs.apitally.io).
|
|
34
34
|
|
|
35
35
|
> [!IMPORTANT]
|
|
36
|
-
> **Upgrading from 0.x?** Version 1.0 is a full rewrite with a new setup API. See the [migration guide](MIGRATION.md) for
|
|
36
|
+
> **Upgrading from 0.x?** Version 1.0 is a full rewrite with a new setup API. See the [migration guide](MIGRATION.md) for how to upgrade.
|
|
37
37
|
|
|
38
38
|
## Key features
|
|
39
39
|
|
|
@@ -62,13 +62,13 @@ The SDK supports **Node.js** `>= 20.6` and **Bun** `>= 1.2.0`.
|
|
|
62
62
|
|
|
63
63
|
\* For Hono on Cloudflare Workers use our [Serverless SDK](https://github.com/apitally/apitally-js-serverless) instead.
|
|
64
64
|
|
|
65
|
-
Apitally also supports many other web frameworks in [Python](https://github.com/apitally/apitally-py), [Go](https://github.com/apitally/apitally-go)
|
|
65
|
+
Apitally also supports many other web frameworks in [Python](https://github.com/apitally/apitally-py), [Go](https://github.com/apitally/apitally-go) and [.NET](https://github.com/apitally/apitally-dotnet) via our other SDKs.
|
|
66
66
|
|
|
67
67
|
## Getting started
|
|
68
68
|
|
|
69
69
|
If you don't have an Apitally account yet, first [sign up here](https://app.apitally.io/?signup). Then create an app in the Apitally dashboard. You'll see detailed setup instructions with code snippets you can copy and paste. These also include your write token.
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
Install the SDK in your project:
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
74
|
npm install apitally
|
|
@@ -78,35 +78,32 @@ See the [SDK reference](https://docs.apitally.io/sdk-reference/javascript/v1/con
|
|
|
78
78
|
|
|
79
79
|
### AdonisJS
|
|
80
80
|
|
|
81
|
-
Run the Ace add command from your application directory:
|
|
81
|
+
Run the Ace `add` command from your application directory:
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
84
|
node ace add apitally
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
The command installs
|
|
88
|
-
|
|
89
|
-
If needed, you can change settings in `config/apitally.ts`.
|
|
87
|
+
The command installs Apitally, asks for your write token and environment, and sets up the provider, middleware, and exception handling. To change settings later, edit `config/apitally.ts`.
|
|
90
88
|
|
|
91
89
|
For further instructions, see our [setup guide for AdonisJS](https://docs.apitally.io/sdk-reference/javascript/v1/setup-guides/adonisjs).
|
|
92
90
|
|
|
93
91
|
### Elysia
|
|
94
92
|
|
|
95
|
-
Register `apitallyPlugin()` immediately after creating the app, before
|
|
93
|
+
Register `apitallyPlugin()` immediately after creating the app, before registering plugins and routes:
|
|
96
94
|
|
|
97
95
|
```javascript
|
|
98
96
|
import { Elysia } from "elysia";
|
|
99
97
|
import { apitallyPlugin } from "apitally/elysia";
|
|
100
98
|
|
|
101
|
-
const app = new Elysia()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.get("/items/:id", ({ params }) => ({ id: params.id }));
|
|
99
|
+
const app = new Elysia().use(
|
|
100
|
+
apitallyPlugin({
|
|
101
|
+
writeToken: "your-write-token",
|
|
102
|
+
env: "dev",
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// register plugins and routes below this point
|
|
110
107
|
```
|
|
111
108
|
|
|
112
109
|
For further instructions, see our [setup guide for Elysia](https://docs.apitally.io/sdk-reference/javascript/v1/setup-guides/elysia).
|
|
@@ -124,8 +121,8 @@ import { useApitally } from "apitally";
|
|
|
124
121
|
const app = express();
|
|
125
122
|
|
|
126
123
|
useApitally(app, {
|
|
127
|
-
writeToken: "your-write-token",
|
|
128
|
-
env: "dev",
|
|
124
|
+
writeToken: "your-write-token",
|
|
125
|
+
env: "dev",
|
|
129
126
|
});
|
|
130
127
|
```
|
|
131
128
|
|
|
@@ -142,8 +139,8 @@ import { useApitally } from "apitally";
|
|
|
142
139
|
const app = Fastify();
|
|
143
140
|
|
|
144
141
|
useApitally(app, {
|
|
145
|
-
writeToken: "your-write-token",
|
|
146
|
-
env: "dev",
|
|
142
|
+
writeToken: "your-write-token",
|
|
143
|
+
env: "dev",
|
|
147
144
|
});
|
|
148
145
|
|
|
149
146
|
// register plugins and routes below this point
|
|
@@ -151,30 +148,9 @@ useApitally(app, {
|
|
|
151
148
|
|
|
152
149
|
For further instructions, see our [setup guide for Fastify](https://docs.apitally.io/sdk-reference/javascript/v1/setup-guides/fastify).
|
|
153
150
|
|
|
154
|
-
### NestJS
|
|
155
|
-
|
|
156
|
-
Call `useApitally(app)` immediately after creating the Nest application, before `app.init()` or `app.listen()`:
|
|
157
|
-
|
|
158
|
-
```javascript
|
|
159
|
-
import { NestFactory } from "@nestjs/core";
|
|
160
|
-
import { useApitally } from "apitally/nestjs";
|
|
161
|
-
import { AppModule } from "./app.module.js";
|
|
162
|
-
|
|
163
|
-
const app = await NestFactory.create(AppModule);
|
|
164
|
-
|
|
165
|
-
useApitally(app, {
|
|
166
|
-
writeToken: "your-write-token", // or set APITALLY_WRITE_TOKEN
|
|
167
|
-
env: "dev", // or "prod" etc.
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
await app.listen(3000);
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
For further instructions, see our [setup guide for NestJS](https://docs.apitally.io/sdk-reference/javascript/v1/setup-guides/nestjs).
|
|
174
|
-
|
|
175
151
|
### H3
|
|
176
152
|
|
|
177
|
-
Add `apitallyPlugin()`
|
|
153
|
+
Add `apitallyPlugin()` to the `plugins` of your root H3 app:
|
|
178
154
|
|
|
179
155
|
```javascript
|
|
180
156
|
import { H3 } from "h3";
|
|
@@ -183,8 +159,8 @@ import { apitallyPlugin } from "apitally/h3";
|
|
|
183
159
|
const app = new H3({
|
|
184
160
|
plugins: [
|
|
185
161
|
apitallyPlugin({
|
|
186
|
-
writeToken: "your-write-token",
|
|
187
|
-
env: "dev",
|
|
162
|
+
writeToken: "your-write-token",
|
|
163
|
+
env: "dev",
|
|
188
164
|
}),
|
|
189
165
|
],
|
|
190
166
|
});
|
|
@@ -194,7 +170,7 @@ For further instructions, see our [setup guide for H3](https://docs.apitally.io/
|
|
|
194
170
|
|
|
195
171
|
### Hapi
|
|
196
172
|
|
|
197
|
-
Register `apitallyPlugin()`
|
|
173
|
+
Register `apitallyPlugin()` immediately after creating the server, before registering other plugins and routes:
|
|
198
174
|
|
|
199
175
|
```javascript
|
|
200
176
|
import Hapi from "@hapi/hapi";
|
|
@@ -204,12 +180,12 @@ const server = Hapi.server({ port: 3000 });
|
|
|
204
180
|
|
|
205
181
|
await server.register(
|
|
206
182
|
apitallyPlugin({
|
|
207
|
-
writeToken: "your-write-token",
|
|
208
|
-
env: "dev",
|
|
183
|
+
writeToken: "your-write-token",
|
|
184
|
+
env: "dev",
|
|
209
185
|
}),
|
|
210
186
|
);
|
|
211
187
|
|
|
212
|
-
// register
|
|
188
|
+
// register other plugins and routes below this point
|
|
213
189
|
await server.start();
|
|
214
190
|
```
|
|
215
191
|
|
|
@@ -226,8 +202,8 @@ import { useApitally } from "apitally";
|
|
|
226
202
|
const app = new Hono();
|
|
227
203
|
|
|
228
204
|
useApitally(app, {
|
|
229
|
-
writeToken: "your-write-token",
|
|
230
|
-
env: "dev",
|
|
205
|
+
writeToken: "your-write-token",
|
|
206
|
+
env: "dev",
|
|
231
207
|
});
|
|
232
208
|
|
|
233
209
|
// register middleware and routes below this point
|
|
@@ -240,14 +216,14 @@ For further instructions, see our [setup guide for Hono](https://docs.apitally.i
|
|
|
240
216
|
Call `useApitally(app)` immediately after creating the app, before registering middleware and routes:
|
|
241
217
|
|
|
242
218
|
```javascript
|
|
243
|
-
|
|
244
|
-
|
|
219
|
+
import Koa from "koa";
|
|
220
|
+
import { useApitally } from "apitally";
|
|
245
221
|
|
|
246
222
|
const app = new Koa();
|
|
247
223
|
|
|
248
224
|
useApitally(app, {
|
|
249
|
-
writeToken: "your-write-token",
|
|
250
|
-
env: "dev",
|
|
225
|
+
writeToken: "your-write-token",
|
|
226
|
+
env: "dev",
|
|
251
227
|
});
|
|
252
228
|
|
|
253
229
|
// register middleware and routes below this point
|
|
@@ -255,11 +231,42 @@ useApitally(app, {
|
|
|
255
231
|
|
|
256
232
|
For further instructions, see our [setup guide for Koa](https://docs.apitally.io/sdk-reference/javascript/v1/setup-guides/koa).
|
|
257
233
|
|
|
234
|
+
### NestJS
|
|
235
|
+
|
|
236
|
+
Call `useApitally(app)` immediately after creating the app, before calling `app.init()` or `app.listen()`:
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
import { NestFactory } from "@nestjs/core";
|
|
240
|
+
import { useApitally } from "apitally/nestjs";
|
|
241
|
+
import { AppModule } from "./app.module.js";
|
|
242
|
+
|
|
243
|
+
const app = await NestFactory.create(AppModule);
|
|
244
|
+
|
|
245
|
+
useApitally(app, {
|
|
246
|
+
writeToken: "your-write-token",
|
|
247
|
+
env: "dev",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await app.listen(3000);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
For further instructions, see our [setup guide for NestJS](https://docs.apitally.io/sdk-reference/javascript/v1/setup-guides/nestjs).
|
|
254
|
+
|
|
258
255
|
## Configuration
|
|
259
256
|
|
|
260
|
-
|
|
257
|
+
Pass configuration options to `useApitally()`, or to `apitallyPlugin()` for Elysia, H3, and Hapi. For AdonisJS, set them in `config/apitally.ts`. See the [SDK reference](https://docs.apitally.io/sdk-reference/javascript/v1/configuration) for all available options.
|
|
258
|
+
|
|
259
|
+
### Environment variables
|
|
261
260
|
|
|
262
|
-
|
|
261
|
+
To keep configuration out of your code, set these environment variables instead of passing the matching options:
|
|
262
|
+
|
|
263
|
+
- `APITALLY_WRITE_TOKEN`: the write token (`writeToken`)
|
|
264
|
+
- `APITALLY_ENV`: the environment name (`env`), defaults to `dev`
|
|
265
|
+
- `APITALLY_DISABLED`: set to `true` to disable Apitally (`disabled`)
|
|
266
|
+
|
|
267
|
+
### Request and response data
|
|
268
|
+
|
|
269
|
+
Apitally captures response headers by default. To also capture request headers and request and response bodies, pass these options:
|
|
263
270
|
|
|
264
271
|
```javascript
|
|
265
272
|
useApitally(app, {
|
|
@@ -271,45 +278,72 @@ useApitally(app, {
|
|
|
271
278
|
});
|
|
272
279
|
```
|
|
273
280
|
|
|
274
|
-
|
|
281
|
+
Apitally automatically masks common sensitive values, such as `password` fields and the `Authorization` header. To mask additional values, pass regular expressions to `maskQueryParams`, `maskHeaders`, and `maskBodyFields`. See our [documentation on masking](https://docs.apitally.io/sdk-reference/javascript/v1/masking) for details.
|
|
282
|
+
|
|
283
|
+
### Exclusions and sampling
|
|
284
|
+
|
|
285
|
+
Apitally automatically excludes common health checks and static assets, such as `/healthz` and `/robots.txt`, from logs and traces. To exclude additional paths, pass regular expressions to `excludePaths`.
|
|
286
|
+
|
|
287
|
+
To capture logs and traces for only a fraction of the remaining requests, set `sampleRate`, for example to `0.1` for 10%. To decide for each request individually, pass a callback to `sampleOnRequest` or `sampleOnResponse`.
|
|
275
288
|
|
|
276
|
-
|
|
289
|
+
Metrics always include all requests. See our [documentation on sampling](https://docs.apitally.io/sdk-reference/javascript/v1/sampling) for details.
|
|
277
290
|
|
|
278
|
-
Application logs
|
|
291
|
+
### Application logs
|
|
279
292
|
|
|
280
|
-
|
|
293
|
+
Apitally captures logs written with these loggers and correlates them with the requests they belong to:
|
|
281
294
|
|
|
282
|
-
|
|
295
|
+
- `console`
|
|
296
|
+
- Pino
|
|
297
|
+
- Winston
|
|
298
|
+
- NestJS `Logger`
|
|
299
|
+
- Hapi `request.log()`
|
|
283
300
|
|
|
284
|
-
|
|
301
|
+
To mask or drop individual log records, pass a callback to `maskLogRecord`. To turn off log capture, set `captureLogs: false`.
|
|
302
|
+
|
|
303
|
+
See our [documentation on masking](https://docs.apitally.io/sdk-reference/javascript/v1/masking#log-record-masking-callback) for details.
|
|
304
|
+
|
|
305
|
+
## Adding context to requests
|
|
306
|
+
|
|
307
|
+
The following functions add context to the current request. Call them while handling a request, for example in a route handler, middleware, or authentication code.
|
|
308
|
+
|
|
309
|
+
To associate the request with the API consumer making it, call `setConsumer`. Only `identifier` is required. You can also pass it as a string, for example `setConsumer(user.identifier)`.
|
|
285
310
|
|
|
286
311
|
```javascript
|
|
287
|
-
import { setConsumer
|
|
312
|
+
import { setConsumer } from "apitally";
|
|
288
313
|
|
|
289
|
-
// Associate the current request with an API consumer, with custom attributes
|
|
290
314
|
setConsumer({
|
|
291
315
|
identifier: user.identifier,
|
|
292
316
|
name: user.name,
|
|
293
317
|
group: user.group,
|
|
294
318
|
attributes: { plan: user.plan },
|
|
295
319
|
});
|
|
320
|
+
```
|
|
296
321
|
|
|
297
|
-
|
|
298
|
-
setRequestAttribute("tenant", tenantId);
|
|
322
|
+
To attach custom data to the request's trace, call `setRequestAttribute`. Sampling and masking callbacks can also read these attributes.
|
|
299
323
|
|
|
300
|
-
|
|
301
|
-
|
|
324
|
+
```javascript
|
|
325
|
+
import { setRequestAttribute } from "apitally";
|
|
326
|
+
|
|
327
|
+
setRequestAttribute("tenant.id", tenantId);
|
|
302
328
|
```
|
|
303
329
|
|
|
304
|
-
|
|
330
|
+
To capture an exception that your code handles itself, call `captureException`. Unhandled exceptions are captured automatically.
|
|
305
331
|
|
|
306
|
-
|
|
332
|
+
```javascript
|
|
333
|
+
import { captureException } from "apitally";
|
|
307
334
|
|
|
308
|
-
|
|
335
|
+
try {
|
|
336
|
+
processOrder();
|
|
337
|
+
} catch (error) {
|
|
338
|
+
captureException(error);
|
|
339
|
+
}
|
|
340
|
+
```
|
|
309
341
|
|
|
310
|
-
|
|
342
|
+
See our documentation on [custom attributes](https://docs.apitally.io/sdk-reference/javascript/v1/attributes#set-custom-attributes) and [handled exceptions](https://docs.apitally.io/sdk-reference/javascript/v1/tracing#capture-handled-exceptions) for details.
|
|
311
343
|
|
|
312
|
-
|
|
344
|
+
## Existing OpenTelemetry setup
|
|
345
|
+
|
|
346
|
+
If your app already registers its own tracer provider, for example with `NodeSDK`, add `ApitallySpanProcessor` to its span processors. Apitally then uses your tracer provider instead of registering its own, and keeps your existing exporters.
|
|
313
347
|
|
|
314
348
|
```javascript
|
|
315
349
|
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
@@ -321,40 +355,29 @@ const sdk = new NodeSDK({
|
|
|
321
355
|
});
|
|
322
356
|
```
|
|
323
357
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
### Elysia's OpenTelemetry plugin
|
|
327
|
-
|
|
328
|
-
When using Elysia's `@elysia/opentelemetry` plugin, register Apitally first so it adopts the OpenTelemetry SERVER span:
|
|
329
|
-
|
|
330
|
-
```javascript
|
|
331
|
-
import { opentelemetry } from "@elysia/opentelemetry";
|
|
332
|
-
import { Elysia } from "elysia";
|
|
333
|
-
import { apitallyPlugin } from "apitally/elysia";
|
|
334
|
-
|
|
335
|
-
const app = new Elysia()
|
|
336
|
-
.use(apitallyPlugin(options))
|
|
337
|
-
.use(opentelemetry());
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
## Trusted proxies
|
|
358
|
+
If you use versions 8 to 10 of the Sentry SDK, which register their own tracer provider, add `ApitallySpanProcessor` to the `openTelemetrySpanProcessors` option in `Sentry.init()` and enable Sentry tracing.
|
|
341
359
|
|
|
342
|
-
|
|
360
|
+
Your tracer provider's sampler also applies to Apitally. Requests it drops have no request logs or traces in Apitally, but still count in metrics.
|
|
343
361
|
|
|
344
362
|
## Graceful shutdown
|
|
345
363
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
If your code calls `process.exit()`, await `shutdown()` first to give Apitally time to send buffered telemetry. Close your server and let any remaining requests finish before doing this:
|
|
364
|
+
If your code calls `process.exit()`, await `shutdown()` before it so Apitally can send any remaining telemetry:
|
|
349
365
|
|
|
350
366
|
```javascript
|
|
351
367
|
import { shutdown } from "apitally";
|
|
352
368
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
369
|
+
process.on("SIGTERM", () => {
|
|
370
|
+
server.close(async () => {
|
|
371
|
+
await shutdown();
|
|
372
|
+
process.exit(0);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
356
375
|
```
|
|
357
376
|
|
|
377
|
+
## Trusted proxies
|
|
378
|
+
|
|
379
|
+
If your app runs behind a reverse proxy or load balancer, configure trusted proxies in your framework, for example with Express's `trust proxy` setting or Fastify's `trustProxy` option. Apitally records the client IP your framework reports and uses it for GeoIP.
|
|
380
|
+
|
|
358
381
|
## Getting help
|
|
359
382
|
|
|
360
383
|
If you need help please [create a new discussion](https://github.com/orgs/apitally/discussions/categories/q-a) on GitHub or email us at [support@apitally.io](mailto:support@apitally.io). We'll get back to you as soon as possible.
|
package/dist/activation.cjs
CHANGED
|
@@ -7,11 +7,11 @@ const require_logCapture = require("./logCapture.cjs");
|
|
|
7
7
|
const require_logRecordExporter = require("./logRecordExporter.cjs");
|
|
8
8
|
const require_logRecordProcessor = require("./logRecordProcessor.cjs");
|
|
9
9
|
const require_metrics = require("./metrics.cjs");
|
|
10
|
+
const require_spanProcessor = require("./spanProcessor.cjs");
|
|
10
11
|
const require_providers = require("./providers.cjs");
|
|
11
12
|
const require_redaction = require("./redaction.cjs");
|
|
12
13
|
const require_sentry = require("./sentry.cjs");
|
|
13
14
|
const require_serverErrors = require("./serverErrors.cjs");
|
|
14
|
-
const require_spanProcessor = require("./spanProcessor.cjs");
|
|
15
15
|
const require_spanExporter = require("./spanExporter.cjs");
|
|
16
16
|
const require_spool = require("./spool.cjs");
|
|
17
17
|
const require_startup = require("./startup.cjs");
|
package/dist/activation.js
CHANGED
|
@@ -6,11 +6,11 @@ import { installConsoleCapture, installNestLoggerCapture, installPinoCapture, in
|
|
|
6
6
|
import { ApitallyLogRecordExporter } from "./logRecordExporter.js";
|
|
7
7
|
import { ApitallyLogRecordProcessor } from "./logRecordProcessor.js";
|
|
8
8
|
import { MetricsPipeline } from "./metrics.js";
|
|
9
|
+
import { SpanPipeline, isApitallySpanProcessorDeclared, setActiveSpanPipeline, setServerSpanActivationCallback } from "./spanProcessor.js";
|
|
9
10
|
import { createLoggerProvider, resolveEnvAndCreateResource, setupTracerProvider } from "./providers.js";
|
|
10
11
|
import { Redaction } from "./redaction.js";
|
|
11
12
|
import { installSentryEventIdRecording } from "./sentry.js";
|
|
12
13
|
import { SERVER_ERROR_EVENT_NAME, drainServerErrors, resetServerErrors } from "./serverErrors.js";
|
|
13
|
-
import { SpanPipeline, isApitallySpanProcessorDeclared, setActiveSpanPipeline, setServerSpanActivationCallback } from "./spanProcessor.js";
|
|
14
14
|
import { ApitallySpanExporter } from "./spanExporter.js";
|
|
15
15
|
import { Spool } from "./spool.js";
|
|
16
16
|
import { emitStartupEvent } from "./startup.js";
|
package/dist/config.cjs
CHANGED
|
@@ -26,6 +26,8 @@ const DEFAULT_MASK_BODY_FIELDS = [
|
|
|
26
26
|
"auth",
|
|
27
27
|
"card[-_ ]?number",
|
|
28
28
|
"ccv",
|
|
29
|
+
"cvv",
|
|
30
|
+
"cvc",
|
|
29
31
|
"ssn"
|
|
30
32
|
];
|
|
31
33
|
const DEFAULT_EXCLUDE_PATHS = [
|
|
@@ -77,10 +79,11 @@ function isValidWriteToken(value) {
|
|
|
77
79
|
return WRITE_TOKEN_FORMAT.test(value);
|
|
78
80
|
}
|
|
79
81
|
function matchesAny(patterns, value) {
|
|
80
|
-
|
|
82
|
+
for (const pattern of patterns) {
|
|
81
83
|
pattern.lastIndex = 0;
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
+
if (pattern.test(value)) return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
84
87
|
}
|
|
85
88
|
function compilePatterns(defaults, userPatterns = []) {
|
|
86
89
|
return [...defaults.map((pattern) => new RegExp(pattern, "i")), ...userPatterns.map((pattern) => new RegExp(pattern))];
|
package/dist/config.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.cjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["import type { ReadWriteLogRecord } from \"@opentelemetry/sdk-logs\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { logError, logWarning } from \"./logger.js\";\n\nexport type BodyMaskingCallback = (body: Buffer, span: ReadableSpan) => Buffer | null;\nexport type LogRecordMaskingCallback = (\n logRecord: ReadWriteLogRecord,\n) => ReadWriteLogRecord | null | undefined;\nexport type SamplingCallback = (span: ReadableSpan) => number | boolean | undefined;\n\nexport interface ApitallyOptions {\n writeToken?: string;\n env?: string;\n appVersion?: string;\n disabled?: boolean;\n captureLogs?: boolean;\n captureRequestHeaders?: boolean;\n captureRequestBody?: boolean;\n captureResponseHeaders?: boolean;\n captureResponseBody?: boolean;\n maskQueryParams?: RegExp[];\n maskHeaders?: RegExp[];\n maskBodyFields?: RegExp[];\n maskRequestBody?: BodyMaskingCallback;\n maskResponseBody?: BodyMaskingCallback;\n maskLogRecord?: LogRecordMaskingCallback;\n excludePaths?: RegExp[];\n sampleRate?: number;\n sampleOnRequest?: SamplingCallback;\n sampleOnResponse?: SamplingCallback;\n}\n\ntype OptionalConfigKeys =\n | \"appVersion\"\n | \"maskRequestBody\"\n | \"maskResponseBody\"\n | \"maskLogRecord\"\n | \"sampleOnRequest\"\n | \"sampleOnResponse\";\n\nexport type ApitallyConfig = Required<Omit<ApitallyOptions, OptionalConfigKeys>> &\n Pick<ApitallyOptions, OptionalConfigKeys> & { otlpEndpoint: string };\n\nconst DEFAULT_OTLP_ENDPOINT = \"https://otlp.apitally.io\";\nexport const DEFAULT_ENV = \"dev\";\n\n// User-supplied patterns extend these defaults, never replace them.\nexport const DEFAULT_MASK_QUERY_PARAMS = [\n \"auth\",\n \"api[-_]?key\",\n \"secret\",\n \"token\",\n \"password\",\n \"pwd\",\n];\nexport const DEFAULT_MASK_HEADERS = [\"auth\", \"api[-_]?key\", \"secret\", \"token\", \"cookie\"];\nexport const DEFAULT_MASK_BODY_FIELDS = [\n \"password\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"auth\",\n \"card[-_ ]?number\",\n \"ccv\",\n \"ssn\",\n];\nexport const DEFAULT_EXCLUDE_PATHS = [\n \"/_?healthz?/?$\",\n \"/_?health[-_]?checks?/?$\",\n \"/_?heart[-_]?beats?/?$\",\n \"/ping/?$\",\n \"/ready/?$\",\n \"/live/?$\",\n \"/favicon(?:-[\\\\w-]+)?\\\\.(ico|png|svg)$\",\n \"/apple-touch-icon(?:-[\\\\w-]+)?\\\\.png$\",\n \"/robots\\\\.txt$\",\n \"/sitemap\\\\.xml$\",\n \"/manifest\\\\.json$\",\n \"/site\\\\.webmanifest$\",\n \"/service-worker\\\\.js$\",\n \"/sw\\\\.js$\",\n \"/\\\\.well-known/\",\n];\nexport const EXCLUDE_USER_AGENTS = [\n \"health[-_ ]?check\",\n \"microsoft-azure-application-lb\",\n \"googlehc\",\n \"kube-probe\",\n];\n\nconst WRITE_TOKEN_FORMAT = /^apt_[a-zA-Z0-9]{24}$/;\nconst TRUE_VALUES = new Set([\"1\", \"true\", \"yes\"]);\n\n// The ESM and CJS builds can load together, so a Symbol.for key gives both\n// copies the same configuration.\nconst GLOBAL_CONFIG_KEY = Symbol.for(\"apitally.config\");\nconst configHolder = globalThis as Record<symbol, ApitallyConfig | undefined>;\n\nexport function setConfig(options: ApitallyOptions = {}): ApitallyConfig {\n const { config, error } = resolveConfig(options);\n const currentConfig = configHolder[GLOBAL_CONFIG_KEY];\n if (currentConfig) {\n if (!isSameConfig(config, currentConfig)) {\n logWarning(\n \"useApitally() was called again with different options; the first call's configuration stays in effect\",\n );\n }\n return currentConfig;\n }\n if (error) {\n logError(error);\n }\n configHolder[GLOBAL_CONFIG_KEY] = config;\n return config;\n}\n\nexport function getConfig(): ApitallyConfig {\n return configHolder[GLOBAL_CONFIG_KEY] ?? resolveConfig({}).config;\n}\n\nexport function isValidWriteToken(value: string): boolean {\n return WRITE_TOKEN_FORMAT.test(value);\n}\n\nexport function matchesAny(patterns: RegExp[], value: string): boolean {\n return patterns.some((pattern) => {\n pattern.lastIndex = 0;\n return pattern.test(value);\n });\n}\n\nexport function compilePatterns(defaults: string[], userPatterns: RegExp[] = []): RegExp[] {\n return [\n ...defaults.map((pattern) => new RegExp(pattern, \"i\")),\n ...userPatterns.map((pattern) => new RegExp(pattern)),\n ];\n}\n\nfunction resolveConfig(options: ApitallyOptions): {\n config: ApitallyConfig;\n error?: string;\n} {\n const config: ApitallyConfig = {\n writeToken: options.writeToken ?? nonEmptyEnvVar(\"APITALLY_WRITE_TOKEN\") ?? \"\",\n env: options.env ?? nonEmptyEnvVar(\"APITALLY_ENV\") ?? DEFAULT_ENV,\n appVersion: options.appVersion,\n disabled:\n options.disabled === true ||\n isTruthyEnvValue(process.env.APITALLY_DISABLED) ||\n isTruthyEnvValue(process.env.OTEL_SDK_DISABLED),\n captureLogs: options.captureLogs ?? true,\n captureRequestHeaders: options.captureRequestHeaders ?? false,\n captureRequestBody: options.captureRequestBody ?? false,\n captureResponseHeaders: options.captureResponseHeaders ?? true,\n captureResponseBody: options.captureResponseBody ?? false,\n maskQueryParams: dropNonRegExpPatterns(\"maskQueryParams\", options.maskQueryParams),\n maskHeaders: dropNonRegExpPatterns(\"maskHeaders\", options.maskHeaders),\n maskBodyFields: dropNonRegExpPatterns(\"maskBodyFields\", options.maskBodyFields),\n maskRequestBody: options.maskRequestBody,\n maskResponseBody: options.maskResponseBody,\n maskLogRecord: options.maskLogRecord,\n excludePaths: dropNonRegExpPatterns(\"excludePaths\", options.excludePaths),\n // An invalid sampleRate resolves to capturing everything: no data is lost, so no warning.\n sampleRate:\n typeof options.sampleRate === \"number\" && options.sampleRate >= 0 && options.sampleRate <= 1\n ? options.sampleRate\n : 1,\n sampleOnRequest: options.sampleOnRequest,\n sampleOnResponse: options.sampleOnResponse,\n otlpEndpoint: nonEmptyEnvVar(\"APITALLY_OTLP_ENDPOINT\") ?? DEFAULT_OTLP_ENDPOINT,\n };\n if (config.disabled) {\n return { config };\n }\n if (!config.writeToken) {\n config.disabled = true;\n return {\n config,\n error:\n \"Apitally write token is missing (set the writeToken option or the APITALLY_WRITE_TOKEN environment variable)\",\n };\n }\n if (!isValidWriteToken(config.writeToken)) {\n config.disabled = true;\n // The write token is a credential and must never appear unmasked in logs.\n return {\n config,\n error: `Apitally write token has an invalid format: ${config.writeToken.slice(0, 8)}...`,\n };\n }\n if (!isHttpUrl(config.otlpEndpoint)) {\n config.disabled = true;\n return {\n config,\n error: `Apitally OTLP endpoint is not a valid HTTP or HTTPS URL: ${config.otlpEndpoint}`,\n };\n }\n return { config };\n}\n\nfunction isHttpUrl(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n}\n\n// Invalid pattern values could leave data unredacted, so they are logged and omitted.\nfunction dropNonRegExpPatterns(optionName: string, patterns: RegExp[] = []): RegExp[] {\n return patterns.filter((pattern) => {\n if (pattern instanceof RegExp) {\n return true;\n }\n logError(`Pattern in ${optionName} ignored because it is not a RegExp: ${String(pattern)}`);\n return false;\n });\n}\n\nfunction isSameConfig(a: ApitallyConfig, b: ApitallyConfig): boolean {\n return (Object.keys(a) as (keyof ApitallyConfig)[]).every((key) => {\n const left = a[key];\n const right = b[key];\n if (Array.isArray(left) && Array.isArray(right)) {\n return (\n left.length === right.length &&\n left.every((item, index) => {\n const other = right[index];\n return (\n item instanceof RegExp &&\n other instanceof RegExp &&\n item.source === other.source &&\n item.flags === other.flags\n );\n })\n );\n }\n // Callback identity says nothing about sameness; app factories create them inline.\n if (typeof left === \"function\" && typeof right === \"function\") {\n return true;\n }\n return left === right;\n });\n}\n\nfunction isTruthyEnvValue(value: string | undefined): boolean {\n return value !== undefined && TRUE_VALUES.has(value.trim().toLowerCase());\n}\n\nfunction nonEmptyEnvVar(name: string): string | undefined {\n return process.env[name]?.trim() || undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"config.cjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["import type { ReadWriteLogRecord } from \"@opentelemetry/sdk-logs\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { logError, logWarning } from \"./logger.js\";\n\nexport type BodyMaskingCallback = (body: Buffer, span: ReadableSpan) => Buffer | null;\nexport type LogRecordMaskingCallback = (\n logRecord: ReadWriteLogRecord,\n) => ReadWriteLogRecord | null | undefined;\nexport type SamplingCallback = (span: ReadableSpan) => number | boolean | undefined;\n\nexport interface ApitallyOptions {\n writeToken?: string;\n env?: string;\n appVersion?: string;\n disabled?: boolean;\n captureLogs?: boolean;\n captureRequestHeaders?: boolean;\n captureRequestBody?: boolean;\n captureResponseHeaders?: boolean;\n captureResponseBody?: boolean;\n maskQueryParams?: RegExp[];\n maskHeaders?: RegExp[];\n maskBodyFields?: RegExp[];\n maskRequestBody?: BodyMaskingCallback;\n maskResponseBody?: BodyMaskingCallback;\n maskLogRecord?: LogRecordMaskingCallback;\n excludePaths?: RegExp[];\n sampleRate?: number;\n sampleOnRequest?: SamplingCallback;\n sampleOnResponse?: SamplingCallback;\n}\n\ntype OptionalConfigKeys =\n | \"appVersion\"\n | \"maskRequestBody\"\n | \"maskResponseBody\"\n | \"maskLogRecord\"\n | \"sampleOnRequest\"\n | \"sampleOnResponse\";\n\nexport type ApitallyConfig = Required<Omit<ApitallyOptions, OptionalConfigKeys>> &\n Pick<ApitallyOptions, OptionalConfigKeys> & { otlpEndpoint: string };\n\nconst DEFAULT_OTLP_ENDPOINT = \"https://otlp.apitally.io\";\nexport const DEFAULT_ENV = \"dev\";\n\n// User-supplied patterns extend these defaults, never replace them.\nexport const DEFAULT_MASK_QUERY_PARAMS = [\n \"auth\",\n \"api[-_]?key\",\n \"secret\",\n \"token\",\n \"password\",\n \"pwd\",\n];\nexport const DEFAULT_MASK_HEADERS = [\"auth\", \"api[-_]?key\", \"secret\", \"token\", \"cookie\"];\nexport const DEFAULT_MASK_BODY_FIELDS = [\n \"password\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"auth\",\n \"card[-_ ]?number\",\n \"ccv\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n];\nexport const DEFAULT_EXCLUDE_PATHS = [\n \"/_?healthz?/?$\",\n \"/_?health[-_]?checks?/?$\",\n \"/_?heart[-_]?beats?/?$\",\n \"/ping/?$\",\n \"/ready/?$\",\n \"/live/?$\",\n \"/favicon(?:-[\\\\w-]+)?\\\\.(ico|png|svg)$\",\n \"/apple-touch-icon(?:-[\\\\w-]+)?\\\\.png$\",\n \"/robots\\\\.txt$\",\n \"/sitemap\\\\.xml$\",\n \"/manifest\\\\.json$\",\n \"/site\\\\.webmanifest$\",\n \"/service-worker\\\\.js$\",\n \"/sw\\\\.js$\",\n \"/\\\\.well-known/\",\n];\nexport const EXCLUDE_USER_AGENTS = [\n \"health[-_ ]?check\",\n \"microsoft-azure-application-lb\",\n \"googlehc\",\n \"kube-probe\",\n];\n\nconst WRITE_TOKEN_FORMAT = /^apt_[a-zA-Z0-9]{24}$/;\nconst TRUE_VALUES = new Set([\"1\", \"true\", \"yes\"]);\n\n// The ESM and CJS builds can load together, so a Symbol.for key gives both\n// copies the same configuration.\nconst GLOBAL_CONFIG_KEY = Symbol.for(\"apitally.config\");\nconst configHolder = globalThis as Record<symbol, ApitallyConfig | undefined>;\n\nexport function setConfig(options: ApitallyOptions = {}): ApitallyConfig {\n const { config, error } = resolveConfig(options);\n const currentConfig = configHolder[GLOBAL_CONFIG_KEY];\n if (currentConfig) {\n if (!isSameConfig(config, currentConfig)) {\n logWarning(\n \"useApitally() was called again with different options; the first call's configuration stays in effect\",\n );\n }\n return currentConfig;\n }\n if (error) {\n logError(error);\n }\n configHolder[GLOBAL_CONFIG_KEY] = config;\n return config;\n}\n\nexport function getConfig(): ApitallyConfig {\n return configHolder[GLOBAL_CONFIG_KEY] ?? resolveConfig({}).config;\n}\n\nexport function isValidWriteToken(value: string): boolean {\n return WRITE_TOKEN_FORMAT.test(value);\n}\n\nexport function matchesAny(patterns: RegExp[], value: string): boolean {\n for (const pattern of patterns) {\n pattern.lastIndex = 0;\n if (pattern.test(value)) {\n return true;\n }\n }\n return false;\n}\n\nexport function compilePatterns(defaults: string[], userPatterns: RegExp[] = []): RegExp[] {\n return [\n ...defaults.map((pattern) => new RegExp(pattern, \"i\")),\n ...userPatterns.map((pattern) => new RegExp(pattern)),\n ];\n}\n\nfunction resolveConfig(options: ApitallyOptions): {\n config: ApitallyConfig;\n error?: string;\n} {\n const config: ApitallyConfig = {\n writeToken: options.writeToken ?? nonEmptyEnvVar(\"APITALLY_WRITE_TOKEN\") ?? \"\",\n env: options.env ?? nonEmptyEnvVar(\"APITALLY_ENV\") ?? DEFAULT_ENV,\n appVersion: options.appVersion,\n disabled:\n options.disabled === true ||\n isTruthyEnvValue(process.env.APITALLY_DISABLED) ||\n isTruthyEnvValue(process.env.OTEL_SDK_DISABLED),\n captureLogs: options.captureLogs ?? true,\n captureRequestHeaders: options.captureRequestHeaders ?? false,\n captureRequestBody: options.captureRequestBody ?? false,\n captureResponseHeaders: options.captureResponseHeaders ?? true,\n captureResponseBody: options.captureResponseBody ?? false,\n maskQueryParams: dropNonRegExpPatterns(\"maskQueryParams\", options.maskQueryParams),\n maskHeaders: dropNonRegExpPatterns(\"maskHeaders\", options.maskHeaders),\n maskBodyFields: dropNonRegExpPatterns(\"maskBodyFields\", options.maskBodyFields),\n maskRequestBody: options.maskRequestBody,\n maskResponseBody: options.maskResponseBody,\n maskLogRecord: options.maskLogRecord,\n excludePaths: dropNonRegExpPatterns(\"excludePaths\", options.excludePaths),\n // An invalid sampleRate resolves to capturing everything: no data is lost, so no warning.\n sampleRate:\n typeof options.sampleRate === \"number\" && options.sampleRate >= 0 && options.sampleRate <= 1\n ? options.sampleRate\n : 1,\n sampleOnRequest: options.sampleOnRequest,\n sampleOnResponse: options.sampleOnResponse,\n otlpEndpoint: nonEmptyEnvVar(\"APITALLY_OTLP_ENDPOINT\") ?? DEFAULT_OTLP_ENDPOINT,\n };\n if (config.disabled) {\n return { config };\n }\n if (!config.writeToken) {\n config.disabled = true;\n return {\n config,\n error:\n \"Apitally write token is missing (set the writeToken option or the APITALLY_WRITE_TOKEN environment variable)\",\n };\n }\n if (!isValidWriteToken(config.writeToken)) {\n config.disabled = true;\n // The write token is a credential and must never appear unmasked in logs.\n return {\n config,\n error: `Apitally write token has an invalid format: ${config.writeToken.slice(0, 8)}...`,\n };\n }\n if (!isHttpUrl(config.otlpEndpoint)) {\n config.disabled = true;\n return {\n config,\n error: `Apitally OTLP endpoint is not a valid HTTP or HTTPS URL: ${config.otlpEndpoint}`,\n };\n }\n return { config };\n}\n\nfunction isHttpUrl(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n}\n\n// Invalid pattern values could leave data unredacted, so they are logged and omitted.\nfunction dropNonRegExpPatterns(optionName: string, patterns: RegExp[] = []): RegExp[] {\n return patterns.filter((pattern) => {\n if (pattern instanceof RegExp) {\n return true;\n }\n logError(`Pattern in ${optionName} ignored because it is not a RegExp: ${String(pattern)}`);\n return false;\n });\n}\n\nfunction isSameConfig(a: ApitallyConfig, b: ApitallyConfig): boolean {\n return (Object.keys(a) as (keyof ApitallyConfig)[]).every((key) => {\n const left = a[key];\n const right = b[key];\n if (Array.isArray(left) && Array.isArray(right)) {\n return (\n left.length === right.length &&\n left.every((item, index) => {\n const other = right[index];\n return (\n item instanceof RegExp &&\n other instanceof RegExp &&\n item.source === other.source &&\n item.flags === other.flags\n );\n })\n );\n }\n // Callback identity says nothing about sameness; app factories create them inline.\n if (typeof left === \"function\" && typeof right === \"function\") {\n return true;\n }\n return left === right;\n });\n}\n\nfunction isTruthyEnvValue(value: string | undefined): boolean {\n return value !== undefined && TRUE_VALUES.has(value.trim().toLowerCase());\n}\n\nfunction nonEmptyEnvVar(name: string): string | undefined {\n return process.env[name]?.trim() || undefined;\n}\n"],"mappings":"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"}
|
package/dist/config.js
CHANGED
|
@@ -25,6 +25,8 @@ const DEFAULT_MASK_BODY_FIELDS = [
|
|
|
25
25
|
"auth",
|
|
26
26
|
"card[-_ ]?number",
|
|
27
27
|
"ccv",
|
|
28
|
+
"cvv",
|
|
29
|
+
"cvc",
|
|
28
30
|
"ssn"
|
|
29
31
|
];
|
|
30
32
|
const DEFAULT_EXCLUDE_PATHS = [
|
|
@@ -76,10 +78,11 @@ function isValidWriteToken(value) {
|
|
|
76
78
|
return WRITE_TOKEN_FORMAT.test(value);
|
|
77
79
|
}
|
|
78
80
|
function matchesAny(patterns, value) {
|
|
79
|
-
|
|
81
|
+
for (const pattern of patterns) {
|
|
80
82
|
pattern.lastIndex = 0;
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
+
if (pattern.test(value)) return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
83
86
|
}
|
|
84
87
|
function compilePatterns(defaults, userPatterns = []) {
|
|
85
88
|
return [...defaults.map((pattern) => new RegExp(pattern, "i")), ...userPatterns.map((pattern) => new RegExp(pattern))];
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","names":[],"sources":["../src/config.ts"],"sourcesContent":["import type { ReadWriteLogRecord } from \"@opentelemetry/sdk-logs\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { logError, logWarning } from \"./logger.js\";\n\nexport type BodyMaskingCallback = (body: Buffer, span: ReadableSpan) => Buffer | null;\nexport type LogRecordMaskingCallback = (\n logRecord: ReadWriteLogRecord,\n) => ReadWriteLogRecord | null | undefined;\nexport type SamplingCallback = (span: ReadableSpan) => number | boolean | undefined;\n\nexport interface ApitallyOptions {\n writeToken?: string;\n env?: string;\n appVersion?: string;\n disabled?: boolean;\n captureLogs?: boolean;\n captureRequestHeaders?: boolean;\n captureRequestBody?: boolean;\n captureResponseHeaders?: boolean;\n captureResponseBody?: boolean;\n maskQueryParams?: RegExp[];\n maskHeaders?: RegExp[];\n maskBodyFields?: RegExp[];\n maskRequestBody?: BodyMaskingCallback;\n maskResponseBody?: BodyMaskingCallback;\n maskLogRecord?: LogRecordMaskingCallback;\n excludePaths?: RegExp[];\n sampleRate?: number;\n sampleOnRequest?: SamplingCallback;\n sampleOnResponse?: SamplingCallback;\n}\n\ntype OptionalConfigKeys =\n | \"appVersion\"\n | \"maskRequestBody\"\n | \"maskResponseBody\"\n | \"maskLogRecord\"\n | \"sampleOnRequest\"\n | \"sampleOnResponse\";\n\nexport type ApitallyConfig = Required<Omit<ApitallyOptions, OptionalConfigKeys>> &\n Pick<ApitallyOptions, OptionalConfigKeys> & { otlpEndpoint: string };\n\nconst DEFAULT_OTLP_ENDPOINT = \"https://otlp.apitally.io\";\nexport const DEFAULT_ENV = \"dev\";\n\n// User-supplied patterns extend these defaults, never replace them.\nexport const DEFAULT_MASK_QUERY_PARAMS = [\n \"auth\",\n \"api[-_]?key\",\n \"secret\",\n \"token\",\n \"password\",\n \"pwd\",\n];\nexport const DEFAULT_MASK_HEADERS = [\"auth\", \"api[-_]?key\", \"secret\", \"token\", \"cookie\"];\nexport const DEFAULT_MASK_BODY_FIELDS = [\n \"password\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"auth\",\n \"card[-_ ]?number\",\n \"ccv\",\n \"ssn\",\n];\nexport const DEFAULT_EXCLUDE_PATHS = [\n \"/_?healthz?/?$\",\n \"/_?health[-_]?checks?/?$\",\n \"/_?heart[-_]?beats?/?$\",\n \"/ping/?$\",\n \"/ready/?$\",\n \"/live/?$\",\n \"/favicon(?:-[\\\\w-]+)?\\\\.(ico|png|svg)$\",\n \"/apple-touch-icon(?:-[\\\\w-]+)?\\\\.png$\",\n \"/robots\\\\.txt$\",\n \"/sitemap\\\\.xml$\",\n \"/manifest\\\\.json$\",\n \"/site\\\\.webmanifest$\",\n \"/service-worker\\\\.js$\",\n \"/sw\\\\.js$\",\n \"/\\\\.well-known/\",\n];\nexport const EXCLUDE_USER_AGENTS = [\n \"health[-_ ]?check\",\n \"microsoft-azure-application-lb\",\n \"googlehc\",\n \"kube-probe\",\n];\n\nconst WRITE_TOKEN_FORMAT = /^apt_[a-zA-Z0-9]{24}$/;\nconst TRUE_VALUES = new Set([\"1\", \"true\", \"yes\"]);\n\n// The ESM and CJS builds can load together, so a Symbol.for key gives both\n// copies the same configuration.\nconst GLOBAL_CONFIG_KEY = Symbol.for(\"apitally.config\");\nconst configHolder = globalThis as Record<symbol, ApitallyConfig | undefined>;\n\nexport function setConfig(options: ApitallyOptions = {}): ApitallyConfig {\n const { config, error } = resolveConfig(options);\n const currentConfig = configHolder[GLOBAL_CONFIG_KEY];\n if (currentConfig) {\n if (!isSameConfig(config, currentConfig)) {\n logWarning(\n \"useApitally() was called again with different options; the first call's configuration stays in effect\",\n );\n }\n return currentConfig;\n }\n if (error) {\n logError(error);\n }\n configHolder[GLOBAL_CONFIG_KEY] = config;\n return config;\n}\n\nexport function getConfig(): ApitallyConfig {\n return configHolder[GLOBAL_CONFIG_KEY] ?? resolveConfig({}).config;\n}\n\nexport function isValidWriteToken(value: string): boolean {\n return WRITE_TOKEN_FORMAT.test(value);\n}\n\nexport function matchesAny(patterns: RegExp[], value: string): boolean {\n return patterns.some((pattern) => {\n pattern.lastIndex = 0;\n return pattern.test(value);\n });\n}\n\nexport function compilePatterns(defaults: string[], userPatterns: RegExp[] = []): RegExp[] {\n return [\n ...defaults.map((pattern) => new RegExp(pattern, \"i\")),\n ...userPatterns.map((pattern) => new RegExp(pattern)),\n ];\n}\n\nfunction resolveConfig(options: ApitallyOptions): {\n config: ApitallyConfig;\n error?: string;\n} {\n const config: ApitallyConfig = {\n writeToken: options.writeToken ?? nonEmptyEnvVar(\"APITALLY_WRITE_TOKEN\") ?? \"\",\n env: options.env ?? nonEmptyEnvVar(\"APITALLY_ENV\") ?? DEFAULT_ENV,\n appVersion: options.appVersion,\n disabled:\n options.disabled === true ||\n isTruthyEnvValue(process.env.APITALLY_DISABLED) ||\n isTruthyEnvValue(process.env.OTEL_SDK_DISABLED),\n captureLogs: options.captureLogs ?? true,\n captureRequestHeaders: options.captureRequestHeaders ?? false,\n captureRequestBody: options.captureRequestBody ?? false,\n captureResponseHeaders: options.captureResponseHeaders ?? true,\n captureResponseBody: options.captureResponseBody ?? false,\n maskQueryParams: dropNonRegExpPatterns(\"maskQueryParams\", options.maskQueryParams),\n maskHeaders: dropNonRegExpPatterns(\"maskHeaders\", options.maskHeaders),\n maskBodyFields: dropNonRegExpPatterns(\"maskBodyFields\", options.maskBodyFields),\n maskRequestBody: options.maskRequestBody,\n maskResponseBody: options.maskResponseBody,\n maskLogRecord: options.maskLogRecord,\n excludePaths: dropNonRegExpPatterns(\"excludePaths\", options.excludePaths),\n // An invalid sampleRate resolves to capturing everything: no data is lost, so no warning.\n sampleRate:\n typeof options.sampleRate === \"number\" && options.sampleRate >= 0 && options.sampleRate <= 1\n ? options.sampleRate\n : 1,\n sampleOnRequest: options.sampleOnRequest,\n sampleOnResponse: options.sampleOnResponse,\n otlpEndpoint: nonEmptyEnvVar(\"APITALLY_OTLP_ENDPOINT\") ?? DEFAULT_OTLP_ENDPOINT,\n };\n if (config.disabled) {\n return { config };\n }\n if (!config.writeToken) {\n config.disabled = true;\n return {\n config,\n error:\n \"Apitally write token is missing (set the writeToken option or the APITALLY_WRITE_TOKEN environment variable)\",\n };\n }\n if (!isValidWriteToken(config.writeToken)) {\n config.disabled = true;\n // The write token is a credential and must never appear unmasked in logs.\n return {\n config,\n error: `Apitally write token has an invalid format: ${config.writeToken.slice(0, 8)}...`,\n };\n }\n if (!isHttpUrl(config.otlpEndpoint)) {\n config.disabled = true;\n return {\n config,\n error: `Apitally OTLP endpoint is not a valid HTTP or HTTPS URL: ${config.otlpEndpoint}`,\n };\n }\n return { config };\n}\n\nfunction isHttpUrl(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n}\n\n// Invalid pattern values could leave data unredacted, so they are logged and omitted.\nfunction dropNonRegExpPatterns(optionName: string, patterns: RegExp[] = []): RegExp[] {\n return patterns.filter((pattern) => {\n if (pattern instanceof RegExp) {\n return true;\n }\n logError(`Pattern in ${optionName} ignored because it is not a RegExp: ${String(pattern)}`);\n return false;\n });\n}\n\nfunction isSameConfig(a: ApitallyConfig, b: ApitallyConfig): boolean {\n return (Object.keys(a) as (keyof ApitallyConfig)[]).every((key) => {\n const left = a[key];\n const right = b[key];\n if (Array.isArray(left) && Array.isArray(right)) {\n return (\n left.length === right.length &&\n left.every((item, index) => {\n const other = right[index];\n return (\n item instanceof RegExp &&\n other instanceof RegExp &&\n item.source === other.source &&\n item.flags === other.flags\n );\n })\n );\n }\n // Callback identity says nothing about sameness; app factories create them inline.\n if (typeof left === \"function\" && typeof right === \"function\") {\n return true;\n }\n return left === right;\n });\n}\n\nfunction isTruthyEnvValue(value: string | undefined): boolean {\n return value !== undefined && TRUE_VALUES.has(value.trim().toLowerCase());\n}\n\nfunction nonEmptyEnvVar(name: string): string | undefined {\n return process.env[name]?.trim() || undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"config.js","names":[],"sources":["../src/config.ts"],"sourcesContent":["import type { ReadWriteLogRecord } from \"@opentelemetry/sdk-logs\";\nimport type { ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport { logError, logWarning } from \"./logger.js\";\n\nexport type BodyMaskingCallback = (body: Buffer, span: ReadableSpan) => Buffer | null;\nexport type LogRecordMaskingCallback = (\n logRecord: ReadWriteLogRecord,\n) => ReadWriteLogRecord | null | undefined;\nexport type SamplingCallback = (span: ReadableSpan) => number | boolean | undefined;\n\nexport interface ApitallyOptions {\n writeToken?: string;\n env?: string;\n appVersion?: string;\n disabled?: boolean;\n captureLogs?: boolean;\n captureRequestHeaders?: boolean;\n captureRequestBody?: boolean;\n captureResponseHeaders?: boolean;\n captureResponseBody?: boolean;\n maskQueryParams?: RegExp[];\n maskHeaders?: RegExp[];\n maskBodyFields?: RegExp[];\n maskRequestBody?: BodyMaskingCallback;\n maskResponseBody?: BodyMaskingCallback;\n maskLogRecord?: LogRecordMaskingCallback;\n excludePaths?: RegExp[];\n sampleRate?: number;\n sampleOnRequest?: SamplingCallback;\n sampleOnResponse?: SamplingCallback;\n}\n\ntype OptionalConfigKeys =\n | \"appVersion\"\n | \"maskRequestBody\"\n | \"maskResponseBody\"\n | \"maskLogRecord\"\n | \"sampleOnRequest\"\n | \"sampleOnResponse\";\n\nexport type ApitallyConfig = Required<Omit<ApitallyOptions, OptionalConfigKeys>> &\n Pick<ApitallyOptions, OptionalConfigKeys> & { otlpEndpoint: string };\n\nconst DEFAULT_OTLP_ENDPOINT = \"https://otlp.apitally.io\";\nexport const DEFAULT_ENV = \"dev\";\n\n// User-supplied patterns extend these defaults, never replace them.\nexport const DEFAULT_MASK_QUERY_PARAMS = [\n \"auth\",\n \"api[-_]?key\",\n \"secret\",\n \"token\",\n \"password\",\n \"pwd\",\n];\nexport const DEFAULT_MASK_HEADERS = [\"auth\", \"api[-_]?key\", \"secret\", \"token\", \"cookie\"];\nexport const DEFAULT_MASK_BODY_FIELDS = [\n \"password\",\n \"pwd\",\n \"token\",\n \"secret\",\n \"auth\",\n \"card[-_ ]?number\",\n \"ccv\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n];\nexport const DEFAULT_EXCLUDE_PATHS = [\n \"/_?healthz?/?$\",\n \"/_?health[-_]?checks?/?$\",\n \"/_?heart[-_]?beats?/?$\",\n \"/ping/?$\",\n \"/ready/?$\",\n \"/live/?$\",\n \"/favicon(?:-[\\\\w-]+)?\\\\.(ico|png|svg)$\",\n \"/apple-touch-icon(?:-[\\\\w-]+)?\\\\.png$\",\n \"/robots\\\\.txt$\",\n \"/sitemap\\\\.xml$\",\n \"/manifest\\\\.json$\",\n \"/site\\\\.webmanifest$\",\n \"/service-worker\\\\.js$\",\n \"/sw\\\\.js$\",\n \"/\\\\.well-known/\",\n];\nexport const EXCLUDE_USER_AGENTS = [\n \"health[-_ ]?check\",\n \"microsoft-azure-application-lb\",\n \"googlehc\",\n \"kube-probe\",\n];\n\nconst WRITE_TOKEN_FORMAT = /^apt_[a-zA-Z0-9]{24}$/;\nconst TRUE_VALUES = new Set([\"1\", \"true\", \"yes\"]);\n\n// The ESM and CJS builds can load together, so a Symbol.for key gives both\n// copies the same configuration.\nconst GLOBAL_CONFIG_KEY = Symbol.for(\"apitally.config\");\nconst configHolder = globalThis as Record<symbol, ApitallyConfig | undefined>;\n\nexport function setConfig(options: ApitallyOptions = {}): ApitallyConfig {\n const { config, error } = resolveConfig(options);\n const currentConfig = configHolder[GLOBAL_CONFIG_KEY];\n if (currentConfig) {\n if (!isSameConfig(config, currentConfig)) {\n logWarning(\n \"useApitally() was called again with different options; the first call's configuration stays in effect\",\n );\n }\n return currentConfig;\n }\n if (error) {\n logError(error);\n }\n configHolder[GLOBAL_CONFIG_KEY] = config;\n return config;\n}\n\nexport function getConfig(): ApitallyConfig {\n return configHolder[GLOBAL_CONFIG_KEY] ?? resolveConfig({}).config;\n}\n\nexport function isValidWriteToken(value: string): boolean {\n return WRITE_TOKEN_FORMAT.test(value);\n}\n\nexport function matchesAny(patterns: RegExp[], value: string): boolean {\n for (const pattern of patterns) {\n pattern.lastIndex = 0;\n if (pattern.test(value)) {\n return true;\n }\n }\n return false;\n}\n\nexport function compilePatterns(defaults: string[], userPatterns: RegExp[] = []): RegExp[] {\n return [\n ...defaults.map((pattern) => new RegExp(pattern, \"i\")),\n ...userPatterns.map((pattern) => new RegExp(pattern)),\n ];\n}\n\nfunction resolveConfig(options: ApitallyOptions): {\n config: ApitallyConfig;\n error?: string;\n} {\n const config: ApitallyConfig = {\n writeToken: options.writeToken ?? nonEmptyEnvVar(\"APITALLY_WRITE_TOKEN\") ?? \"\",\n env: options.env ?? nonEmptyEnvVar(\"APITALLY_ENV\") ?? DEFAULT_ENV,\n appVersion: options.appVersion,\n disabled:\n options.disabled === true ||\n isTruthyEnvValue(process.env.APITALLY_DISABLED) ||\n isTruthyEnvValue(process.env.OTEL_SDK_DISABLED),\n captureLogs: options.captureLogs ?? true,\n captureRequestHeaders: options.captureRequestHeaders ?? false,\n captureRequestBody: options.captureRequestBody ?? false,\n captureResponseHeaders: options.captureResponseHeaders ?? true,\n captureResponseBody: options.captureResponseBody ?? false,\n maskQueryParams: dropNonRegExpPatterns(\"maskQueryParams\", options.maskQueryParams),\n maskHeaders: dropNonRegExpPatterns(\"maskHeaders\", options.maskHeaders),\n maskBodyFields: dropNonRegExpPatterns(\"maskBodyFields\", options.maskBodyFields),\n maskRequestBody: options.maskRequestBody,\n maskResponseBody: options.maskResponseBody,\n maskLogRecord: options.maskLogRecord,\n excludePaths: dropNonRegExpPatterns(\"excludePaths\", options.excludePaths),\n // An invalid sampleRate resolves to capturing everything: no data is lost, so no warning.\n sampleRate:\n typeof options.sampleRate === \"number\" && options.sampleRate >= 0 && options.sampleRate <= 1\n ? options.sampleRate\n : 1,\n sampleOnRequest: options.sampleOnRequest,\n sampleOnResponse: options.sampleOnResponse,\n otlpEndpoint: nonEmptyEnvVar(\"APITALLY_OTLP_ENDPOINT\") ?? DEFAULT_OTLP_ENDPOINT,\n };\n if (config.disabled) {\n return { config };\n }\n if (!config.writeToken) {\n config.disabled = true;\n return {\n config,\n error:\n \"Apitally write token is missing (set the writeToken option or the APITALLY_WRITE_TOKEN environment variable)\",\n };\n }\n if (!isValidWriteToken(config.writeToken)) {\n config.disabled = true;\n // The write token is a credential and must never appear unmasked in logs.\n return {\n config,\n error: `Apitally write token has an invalid format: ${config.writeToken.slice(0, 8)}...`,\n };\n }\n if (!isHttpUrl(config.otlpEndpoint)) {\n config.disabled = true;\n return {\n config,\n error: `Apitally OTLP endpoint is not a valid HTTP or HTTPS URL: ${config.otlpEndpoint}`,\n };\n }\n return { config };\n}\n\nfunction isHttpUrl(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n}\n\n// Invalid pattern values could leave data unredacted, so they are logged and omitted.\nfunction dropNonRegExpPatterns(optionName: string, patterns: RegExp[] = []): RegExp[] {\n return patterns.filter((pattern) => {\n if (pattern instanceof RegExp) {\n return true;\n }\n logError(`Pattern in ${optionName} ignored because it is not a RegExp: ${String(pattern)}`);\n return false;\n });\n}\n\nfunction isSameConfig(a: ApitallyConfig, b: ApitallyConfig): boolean {\n return (Object.keys(a) as (keyof ApitallyConfig)[]).every((key) => {\n const left = a[key];\n const right = b[key];\n if (Array.isArray(left) && Array.isArray(right)) {\n return (\n left.length === right.length &&\n left.every((item, index) => {\n const other = right[index];\n return (\n item instanceof RegExp &&\n other instanceof RegExp &&\n item.source === other.source &&\n item.flags === other.flags\n );\n })\n );\n }\n // Callback identity says nothing about sameness; app factories create them inline.\n if (typeof left === \"function\" && typeof right === \"function\") {\n return true;\n }\n return left === right;\n });\n}\n\nfunction isTruthyEnvValue(value: string | undefined): boolean {\n return value !== undefined && TRUE_VALUES.has(value.trim().toLowerCase());\n}\n\nfunction nonEmptyEnvVar(name: string): string | undefined {\n return process.env[name]?.trim() || undefined;\n}\n"],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_spanProcessor = require("./spanProcessor.cjs");
|
|
2
3
|
const require_requestAttributes = require("./requestAttributes.cjs");
|
|
3
4
|
const require_exceptions = require("./exceptions.cjs");
|
|
4
|
-
const require_spanProcessor = require("./spanProcessor.cjs");
|
|
5
5
|
const require_activation = require("./activation.cjs");
|
|
6
6
|
const require_consumers = require("./consumers.cjs");
|
|
7
7
|
const require_adonisjs_configure = require("./adonisjs/configure.cjs");
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { ApitallySpanProcessor } from "./spanProcessor.js";
|
|
1
2
|
import { setRequestAttribute } from "./requestAttributes.js";
|
|
2
3
|
import { captureException } from "./exceptions.js";
|
|
3
|
-
import { ApitallySpanProcessor } from "./spanProcessor.js";
|
|
4
4
|
import { shutdown } from "./activation.js";
|
|
5
5
|
import { setConsumer } from "./consumers.js";
|
|
6
6
|
import { configure as configure$1 } from "./adonisjs/configure.js";
|
package/dist/metrics.cjs
CHANGED
|
@@ -43,7 +43,6 @@ var MetricsPipeline = class {
|
|
|
43
43
|
if (record.dropReason === "method" || record.dropReason === "scheme") return;
|
|
44
44
|
const source = record.attributes;
|
|
45
45
|
const method = source["http.request.method"] ?? source["http.method"];
|
|
46
|
-
const scheme = source["url.scheme"] ?? source["http.scheme"];
|
|
47
46
|
const route = source["http.route"];
|
|
48
47
|
if (typeof route !== "string" || route === "") return;
|
|
49
48
|
const attributes = { "http.route": route };
|
|
@@ -52,8 +51,6 @@ var MetricsPipeline = class {
|
|
|
52
51
|
if (statusCode !== void 0) attributes["http.response.status_code"] = statusCode;
|
|
53
52
|
const consumer = source["apitally.consumer.identifier"];
|
|
54
53
|
if (consumer !== void 0) attributes["apitally.consumer.identifier"] = consumer;
|
|
55
|
-
if (scheme !== void 0) attributes["url.scheme"] = scheme;
|
|
56
|
-
if (typeof statusCode === "number" && statusCode >= 500) attributes["error.type"] = String(statusCode);
|
|
57
54
|
const key = JSON.stringify(attributes);
|
|
58
55
|
let histograms = this.combinations.get(key);
|
|
59
56
|
if (histograms === void 0) {
|