@forestadmin/agent-bff 1.32.0 → 1.34.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 +107 -5
- package/dist/cli-dispatch.d.ts +1 -1
- package/dist/cli-dispatch.js +6 -11
- package/dist/config/env-config.d.ts +1 -0
- package/dist/config/env-config.js +7 -3
- package/dist/tracing-handle.d.ts +26 -0
- package/dist/tracing-handle.js +40 -0
- package/dist/tracing-preload.d.ts +2 -0
- package/dist/tracing-preload.js +27 -0
- package/dist/tracing.d.ts +79 -0
- package/dist/tracing.js +168 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,108 @@ dropped collection reachable.
|
|
|
36
36
|
Two ways to run it: embedded in a Forest agent (`agent.addBff()`, see
|
|
37
37
|
[Embedded in an agent](#embedded-in-an-agent)) or standalone, described here.
|
|
38
38
|
|
|
39
|
+
### Docker (recommended)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cp .env.example .env # then fill in the secrets
|
|
43
|
+
docker compose up
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The template targets a local, non-containerised run, so one value has to change for Docker:
|
|
47
|
+
set `AGENT_URL=http://host.docker.internal:3351`. Left at `localhost`, it resolves to the BFF
|
|
48
|
+
container itself and every agent call fails (see the note below).
|
|
49
|
+
|
|
50
|
+
The `docker-compose.yml` at the root of this package starts a single BFF instance. See
|
|
51
|
+
`.env.example` for the full list of environment variables and their descriptions.
|
|
52
|
+
|
|
53
|
+
Or run the image directly:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
docker run -d \
|
|
57
|
+
-p 3450:3450 \
|
|
58
|
+
--stop-timeout 15 \
|
|
59
|
+
--add-host host.docker.internal:host-gateway \
|
|
60
|
+
-e FOREST_AUTH_SECRET="..." \
|
|
61
|
+
-e FOREST_ENV_SECRET="..." \
|
|
62
|
+
-e AGENT_URL="http://host.docker.internal:3351" \
|
|
63
|
+
-e BFF_TOKEN_ENCRYPTION_KEY="$(openssl rand -base64 32)" \
|
|
64
|
+
ghcr.io/forestadmin/agent-bff:latest
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> **Note:** When the BFF runs in Docker and your agent runs on the host machine, use
|
|
68
|
+
> `host.docker.internal` instead of `localhost` in `AGENT_URL`. Docker Desktop resolves that
|
|
69
|
+
> name natively; on Docker Engine for Linux it does not exist unless you map it, hence the
|
|
70
|
+
> `--add-host` above (the Compose setup does the same through `extra_hosts`).
|
|
71
|
+
|
|
72
|
+
The image's entry point is the CLI, so the subcommands below work the same way:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
docker run --rm ghcr.io/forestadmin/agent-bff:latest openapi > openapi.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Tags follow the npm package: `:latest`, `:1`, `:1.20` and the immutable `:1.20.2`.
|
|
79
|
+
|
|
80
|
+
The package is public, so none of the commands above need a login. That visibility is set once,
|
|
81
|
+
by hand, on the GHCR package: a package GHCR creates on its first push is private, and the
|
|
82
|
+
workflow's `GITHUB_TOKEN` can push to it but not change what it is. Until someone flips it (the
|
|
83
|
+
same step `workflow-executor` went through), pulls need
|
|
84
|
+
`docker login ghcr.io -u <user> -p <token-with-read:packages>`.
|
|
85
|
+
|
|
86
|
+
On `SIGTERM` or `SIGINT` the BFF stops accepting connections and gives the requests already in
|
|
87
|
+
flight 10 seconds to finish before cutting their sockets, then exits 0. A second signal gives up on
|
|
88
|
+
the wait and exits 1.
|
|
89
|
+
|
|
90
|
+
Allow for that in your orchestrator's grace period. The whole budget is up to 11 seconds — the 10
|
|
91
|
+
second deadline plus a 1 second fallback for the exit itself — and `docker stop` defaults to 10,
|
|
92
|
+
so under load it would SIGKILL exactly when the shutdown is doing its job. Hence `--stop-timeout 15`
|
|
93
|
+
above and `stop_grace_period: 15s` in the Compose file; on Kubernetes the default
|
|
94
|
+
`terminationGracePeriodSeconds` of 30 already covers it.
|
|
95
|
+
|
|
96
|
+
### Observability (OpenTelemetry)
|
|
97
|
+
|
|
98
|
+
The Docker image ships with [OpenTelemetry](https://opentelemetry.io/) APM built in, and works with
|
|
99
|
+
any OTLP-compatible backend (Datadog, Grafana Tempo, Jaeger, Honeycomb, etc.). It is **off by
|
|
100
|
+
default** and turns on as soon as you point it at an OTLP receiver — no code changes or extra
|
|
101
|
+
installs required. A setup that cannot start logs a warning and runs untraced rather than taking
|
|
102
|
+
the process down with it. Tracing is set up before the app starts (auto-instrumentation for HTTP and the
|
|
103
|
+
outbound calls to the agent and the Forest SaaS). The graceful shutdown described above waits for
|
|
104
|
+
the buffered spans to be exported before it exits, but gives that its own 2 second deadline rather
|
|
105
|
+
than the 10 seconds in-flight requests get: an unreachable collector costs you the last spans, never
|
|
106
|
+
the ability to stop. Worst case it adds ~3 seconds to a shutdown.
|
|
107
|
+
|
|
108
|
+
Configure it entirely through the standard OTel environment variables:
|
|
109
|
+
|
|
110
|
+
| Variable | Description |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP receiver URL (e.g. `http://collector:4318`). **Tracing stays off until this or `OTEL_TRACES_EXPORTER` is set.** |
|
|
113
|
+
| `OTEL_SERVICE_NAME` | Service name reported in traces. Falls back to `service.name` in `OTEL_RESOURCE_ATTRIBUTES`, then to `forestadmin-agent-bff`. |
|
|
114
|
+
| `OTEL_RESOURCE_ATTRIBUTES` | Extra resource attributes, e.g. `deployment.environment=production`. A `service.name` here is honoured when `OTEL_SERVICE_NAME` is unset. |
|
|
115
|
+
| `OTEL_SDK_DISABLED` | Set to `true` (case-insensitive) to force-disable tracing whatever else is configured. |
|
|
116
|
+
| `OTEL_TRACES_EXPORTER` | Which exporter the SDK builds: `otlp` (the default), `console`, `zipkin`, `none`, or a list. Setting it alone turns tracing on without an OTLP endpoint, which is what makes `console` usable for debugging. `none` keeps instrumentation running with nothing exported, so trace context still propagates to the agent and the Forest SaaS. |
|
|
117
|
+
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Per-signal endpoint, taking precedence over the generic one above. Setting either turns tracing on. |
|
|
118
|
+
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` (the default), `http/json` or `grpc`, with `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` for traces alone. |
|
|
119
|
+
| `OTEL_METRICS_EXPORTER` / `OTEL_LOGS_EXPORTER` | Default to `none` here, against the SDK's own `otlp`: this image arms tracing, and leaving them unset would otherwise export metrics and logs to `http://localhost:4318` on the side. Set either one (`otlp`, `console`, …) to opt that signal back in. |
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
docker run -d \
|
|
123
|
+
-p 3450:3450 \
|
|
124
|
+
--stop-timeout 15 \
|
|
125
|
+
--add-host host.docker.internal:host-gateway \
|
|
126
|
+
-e FOREST_AUTH_SECRET="..." \
|
|
127
|
+
-e FOREST_ENV_SECRET="..." \
|
|
128
|
+
-e AGENT_URL="http://host.docker.internal:3351" \
|
|
129
|
+
-e BFF_TOKEN_ENCRYPTION_KEY="$(openssl rand -base64 32)" \
|
|
130
|
+
-e OTEL_EXPORTER_OTLP_ENDPOINT="http://collector:4318" \
|
|
131
|
+
ghcr.io/forestadmin/agent-bff:latest
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
> **Note:** These variables only do anything in the Docker image. The image's entry point loads
|
|
135
|
+
> the tracing preload before the CLI; the npm `forest-bff` bin runs the CLI on its own, and the
|
|
136
|
+
> OpenTelemetry packages are not npm dependencies — so outside Docker an `OTEL_*` variable is
|
|
137
|
+
> read by nothing and the process starts untraced, silently.
|
|
138
|
+
|
|
139
|
+
### Without Docker
|
|
140
|
+
|
|
39
141
|
Packaged / production — run the bin:
|
|
40
142
|
|
|
41
143
|
```bash
|
|
@@ -57,7 +159,7 @@ without it a client generated from the export has no base URL at all.
|
|
|
57
159
|
|
|
58
160
|
The document comes in two forms, and the command picks one from the environment:
|
|
59
161
|
|
|
60
|
-
- **Unfolded** when `
|
|
162
|
+
- **Unfolded** when `FOREST_ENV_SECRET`, `FOREST_AUTH_SECRET` and `AGENT_URL`
|
|
61
163
|
are all set: one path per exposed collection, per to-many relation and per action, each carrying
|
|
62
164
|
the collection's real field set. This is the form to generate a client from. The command reads the
|
|
63
165
|
Forest schema and asks the agent for each collection's capabilities, signing its own short-lived
|
|
@@ -95,11 +197,11 @@ yarn start:dev # node --env-file=.env dist/cli.js
|
|
|
95
197
|
| ----------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
96
198
|
| `FOREST_AUTH_SECRET` | yes | Agent JWT signing secret (never logged or echoed). |
|
|
97
199
|
| `FOREST_ENV_SECRET` | yes | Forest SaaS environment secret (`forest-secret-key`), server-to-server. |
|
|
98
|
-
| `FOREST_SERVER_URL` |
|
|
99
|
-
| `FOREST_APP_URL` |
|
|
200
|
+
| `FOREST_SERVER_URL` | no | Forest SaaS API base URL. Defaults to `https://api.forestadmin.com`. |
|
|
201
|
+
| `FOREST_APP_URL` | no | Forest front base URL, used to build the OAuth front-channel redirect (`src/oauth/oauth-routes.ts`). Defaults to `https://app.forestadmin.com`. |
|
|
100
202
|
| `AGENT_URL` | yes | The customer agent base URL the BFF calls via agent-client. |
|
|
101
203
|
| `BFF_TOKEN_ENCRYPTION_KEY` | for OAuth | Base64-encoded 32-byte AES-256 key encrypting stored refresh tokens. Until it is set, the `/oauth/*` token-issuance routes are disabled and `/health` reports `configured.oauth: false` — but it stays `ok`, since the key gates OAuth and not boot; already-issued `bff_access` tokens still authenticate on `/agent/*` whenever `FOREST_AUTH_SECRET` is present. |
|
|
102
|
-
| `HTTP_PORT` | no | Server port, integer 0–65535. Defaults to `3450`. `0` binds an OS-assigned ephemeral port. |
|
|
204
|
+
| `HTTP_PORT` | no | Server port, integer 0–65535. Defaults to `3450`. `0` binds an OS-assigned ephemeral port — useful for a local run, unusable in the Docker image, where nothing outside the process learns which port it got: it can be neither published nor probed, and the image's healthcheck would report the container unhealthy forever. |
|
|
103
205
|
| `BFF_ALLOWED_ORIGINS` | no | Comma-separated CORS allow-list of origins (scheme + host + port). An entry may carry a single `*` as the leading host label — `https://*.apps.zdusercontent.com` — which matches exactly one DNS label there, and nothing else: not two labels, not the apex, and never the scheme or the port. The host left after `*.` must be at least two non-empty labels, so `https://*.com` is refused; a two-label public suffix such as `https://*.co.uk` is not, and would allow every site under it. Any other `*` in the host is refused and warned about at boot; a `*` outside the host — in userinfo, a path or a query — is stripped along with the rest of the URL, so `https://*@example.com` is simply the exact origin `https://example.com`. Empty ⇒ no cross-origin browser access. |
|
|
104
206
|
| `BFF_DEFAULT_TIMEZONE` | no | Fallback IANA timezone used when a request carries neither an `X-Forest-Timezone` header nor a body `timezone`. |
|
|
105
207
|
| `BFF_PUBLIC_URL` | no | The BFF's own external base URL, published as `servers[0].url` in the OpenAPI document so a generated client resolves endpoints without being configured by hand. Absent, `servers[0].url` stays `/`, which a consumer that fetched the document over HTTP resolves against that URL — but which leaves a client generated from an offline `forest-bff openapi` export with no base URL at all. Trailing slashes are stripped. A malformed value fails the boot, and so does one carrying credentials, a query string or a fragment: credentials would be published to every reader of the document, and anything behind a `?` or `#` swallows the path a generated client appends. |
|
|
@@ -274,7 +376,7 @@ answers. They form a chain, not four independent switches:
|
|
|
274
376
|
|
|
275
377
|
| Surface | Switched on by |
|
|
276
378
|
| --- | --- |
|
|
277
|
-
| `oauth` | `BFF_TOKEN_ENCRYPTION_KEY` **and** `FOREST_SERVER_URL`, `FOREST_ENV_SECRET`, `FOREST_APP_URL`, `FOREST_AUTH_SECRET` — the routes need a Forest server to talk to as much as a key. Embedded, only `tokenEncryptionKey` is yours to set: the other four are inherited |
|
|
379
|
+
| `oauth` | `BFF_TOKEN_ENCRYPTION_KEY` **and** `FOREST_SERVER_URL`, `FOREST_ENV_SECRET`, `FOREST_APP_URL`, `FOREST_AUTH_SECRET` — the routes need a Forest server to talk to as much as a key. The two urls default to production, so standalone only the two secrets and the key are yours to set. Embedded, only `tokenEncryptionKey` is yours to set: the other four are inherited |
|
|
278
380
|
| `ai` | `oauth` — the relay needs a session, and only the OAuth flow creates one |
|
|
279
381
|
| `cors` | a non-empty `BFF_ALLOWED_ORIGINS` (`allowedOrigins`) |
|
|
280
382
|
| `openapi` | `BFF_OPENAPI_ENABLED` (`openapiEnabled`), and a mounted agent edge |
|
package/dist/cli-dispatch.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type BFFHttpServer from './http/bff-http-server';
|
|
2
2
|
import type { Logger } from './ports/logger-port';
|
|
3
3
|
export declare const DEFAULT_OUTPUT_FILE = "openapi.json";
|
|
4
|
-
export declare const USAGE = "Usage: forest-bff [command]\n\nCommands:\n (none) Start the BFF server, configured from the environment.\n openapi Write the OpenAPI document to stdout. Needs no configuration, but\n a deployment configured to reach its Forest schema and its agent\n (
|
|
4
|
+
export declare const USAGE = "Usage: forest-bff [command]\n\nCommands:\n (none) Start the BFF server, configured from the environment.\n openapi Write the OpenAPI document to stdout. Needs no configuration, but\n a deployment configured to reach its Forest schema and its agent\n (FOREST_ENV_SECRET, FOREST_AUTH_SECRET, AGENT_URL) unfolds one\n path per collection, relation and action instead of the generic\n ones.\n --output [file] Write to a file instead of stdout, defaulting\n to openapi.json in the current directory.\n\nOptions:\n -h, --help Show this help and exit.\n -v, --version Show the package version and exit.";
|
|
5
5
|
export declare const HINT = "Run 'forest-bff --help' for usage.";
|
|
6
6
|
export interface DispatchOutcome {
|
|
7
7
|
exitCode: number;
|
package/dist/cli-dispatch.js
CHANGED
|
@@ -59,9 +59,9 @@ Commands:
|
|
|
59
59
|
(none) Start the BFF server, configured from the environment.
|
|
60
60
|
openapi Write the OpenAPI document to stdout. Needs no configuration, but
|
|
61
61
|
a deployment configured to reach its Forest schema and its agent
|
|
62
|
-
(
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
(FOREST_ENV_SECRET, FOREST_AUTH_SECRET, AGENT_URL) unfolds one
|
|
63
|
+
path per collection, relation and action instead of the generic
|
|
64
|
+
ones.
|
|
65
65
|
--output [file] Write to a file instead of stdout, defaulting
|
|
66
66
|
to ${exports.DEFAULT_OUTPUT_FILE} in the current directory.
|
|
67
67
|
|
|
@@ -71,12 +71,7 @@ Options:
|
|
|
71
71
|
exports.HINT = "Run 'forest-bff --help' for usage.";
|
|
72
72
|
const HELP_FLAGS = new Set(['-h', '--help']);
|
|
73
73
|
const VERSION_FLAGS = new Set(['-v', '--version']);
|
|
74
|
-
const UNFOLD_VARS = [
|
|
75
|
-
'FOREST_SERVER_URL',
|
|
76
|
-
'FOREST_ENV_SECRET',
|
|
77
|
-
'FOREST_AUTH_SECRET',
|
|
78
|
-
'AGENT_URL',
|
|
79
|
-
];
|
|
74
|
+
const UNFOLD_VARS = ['FOREST_ENV_SECRET', 'FOREST_AUTH_SECRET', 'AGENT_URL'];
|
|
80
75
|
const NOTHING_TO_UNFOLD = `${UNFOLD_VARS.join(', ')} must all be set to unfold it`;
|
|
81
76
|
function wantsUnfolding(env) {
|
|
82
77
|
return UNFOLD_VARS.every(name => (env[name] ?? '').trim() !== '');
|
|
@@ -102,7 +97,7 @@ async function renderOpenApi(env, logger) {
|
|
|
102
97
|
// `parseConfig` validates the WHOLE server configuration, including settings the export has nothing
|
|
103
98
|
// to do with (HTTP_PORT, the OAuth keys, the default timezone). A deployment that never asked for an
|
|
104
99
|
// unfolded document must not see its export die on one of those, so the config is parsed only once
|
|
105
|
-
// the
|
|
100
|
+
// the variables unfolding needs are all present — and from there a bad value is a real failure.
|
|
106
101
|
const unfoldable = authSecret && wantsUnfolding(env)
|
|
107
102
|
? { source: (0, build_bff_1.resolveUnfoldSource)((0, env_config_1.parseConfig)(env), logger), authSecret }
|
|
108
103
|
: undefined;
|
|
@@ -183,4 +178,4 @@ async function dispatchCli(argv, env, logger) {
|
|
|
183
178
|
process.stdout.write(document);
|
|
184
179
|
return { exitCode: 0 };
|
|
185
180
|
}
|
|
186
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
181
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLWRpc3BhdGNoLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2NsaS1kaXNwYXRjaC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUE0RUEsc0NBNENDO0FBZ0RELDhCQWdEQztBQXJORCwyQkFBOEM7QUFDOUMsZ0RBQXdCO0FBRXhCLCtFQUE0RDtBQUM1RCxvRUFBMkQ7QUFDM0QsMkNBQXNFO0FBQ3RFLDBEQUFnQztBQUNoQyxvREFBa0U7QUFDbEUscUNBQStDO0FBQy9DLGlFQUF1RjtBQUN2RixpRkFBNEY7QUFDNUYsbURBQXNEO0FBQ3RELHdEQUFnQztBQUVuQixRQUFBLG1CQUFtQixHQUFHLGNBQWMsQ0FBQztBQUVsRCxNQUFNLFdBQVcsR0FBRyxVQUFVLENBQUM7QUFFbEIsUUFBQSxLQUFLLEdBQUc7Ozs7Ozs7Ozs7b0NBVWUsMkJBQW1COzs7O29EQUlILENBQUM7QUFFeEMsUUFBQSxJQUFJLEdBQUcsb0NBQW9DLENBQUM7QUFFekQsTUFBTSxVQUFVLEdBQUcsSUFBSSxHQUFHLENBQUMsQ0FBQyxJQUFJLEVBQUUsUUFBUSxDQUFDLENBQUMsQ0FBQztBQUM3QyxNQUFNLGFBQWEsR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLElBQUksRUFBRSxXQUFXLENBQUMsQ0FBQyxDQUFDO0FBT25ELE1BQU0sV0FBVyxHQUFHLENBQUMsbUJBQW1CLEVBQUUsb0JBQW9CLEVBQUUsV0FBVyxDQUFVLENBQUM7QUFFdEYsTUFBTSxpQkFBaUIsR0FBRyxHQUFHLFdBQVcsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLCtCQUErQixDQUFDO0FBRW5GLFNBQVMsY0FBYyxDQUFDLEdBQXNCO0lBQzVDLE9BQU8sV0FBVyxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRSxL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
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const REQUIRED_KEYS: readonly ["FOREST_AUTH_SECRET", "FOREST_ENV_SECRET", "FOREST_SERVER_URL", "FOREST_APP_URL", "AGENT_URL"];
|
|
2
2
|
export type RequiredKey = (typeof REQUIRED_KEYS)[number];
|
|
3
|
+
export declare const DEFAULTS: Partial<Record<RequiredKey, string>>;
|
|
3
4
|
export type PresenceMap = Record<RequiredKey, boolean>;
|
|
4
5
|
export interface BFFConfig {
|
|
5
6
|
forestAuthSecret?: string;
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.MIN_RATE_LIMIT_WINDOW_MS = exports.MAX_RATE_LIMIT_REQUESTS = exports.DEFAULT_RATE_LIMIT_WINDOW_MS = exports.DEFAULT_RATE_LIMIT_MAX_REQUESTS = exports.MAX_TIMEOUT_MS = exports.DEFAULT_AI_TIMEOUT_MS = exports.DEFAULT_AGENT_TIMEOUT_MS = exports.REQUIRED_KEYS = void 0;
|
|
6
|
+
exports.MIN_RATE_LIMIT_WINDOW_MS = exports.MAX_RATE_LIMIT_REQUESTS = exports.DEFAULT_RATE_LIMIT_WINDOW_MS = exports.DEFAULT_RATE_LIMIT_MAX_REQUESTS = exports.MAX_TIMEOUT_MS = exports.DEFAULT_AI_TIMEOUT_MS = exports.DEFAULT_AGENT_TIMEOUT_MS = exports.DEFAULTS = exports.REQUIRED_KEYS = void 0;
|
|
7
7
|
exports.parsePublicUrl = parsePublicUrl;
|
|
8
8
|
exports.parseConfig = parseConfig;
|
|
9
9
|
const zod_1 = require("zod");
|
|
@@ -18,6 +18,10 @@ exports.REQUIRED_KEYS = [
|
|
|
18
18
|
'FOREST_APP_URL',
|
|
19
19
|
'AGENT_URL',
|
|
20
20
|
];
|
|
21
|
+
exports.DEFAULTS = {
|
|
22
|
+
FOREST_SERVER_URL: 'https://api.forestadmin.com',
|
|
23
|
+
FOREST_APP_URL: 'https://app.forestadmin.com',
|
|
24
|
+
};
|
|
21
25
|
const URL_KEYS = ['FOREST_SERVER_URL', 'FOREST_APP_URL', 'AGENT_URL'];
|
|
22
26
|
const DECIMAL_INTEGER = /^\d+$/;
|
|
23
27
|
exports.DEFAULT_AGENT_TIMEOUT_MS = 10000;
|
|
@@ -99,7 +103,7 @@ function parseOpenApiEnabled(raw) {
|
|
|
99
103
|
throw new errors_1.ConfigurationError('Invalid configuration: BFF_OPENAPI_ENABLED must be a boolean (true/false).');
|
|
100
104
|
}
|
|
101
105
|
function parseConfig(env) {
|
|
102
|
-
const normalized = Object.fromEntries(exports.REQUIRED_KEYS.map(key => [key, normalize(env[key])]));
|
|
106
|
+
const normalized = Object.fromEntries(exports.REQUIRED_KEYS.map(key => [key, normalize(env[key]) ?? exports.DEFAULTS[key]]));
|
|
103
107
|
for (const key of URL_KEYS) {
|
|
104
108
|
const value = normalized[key];
|
|
105
109
|
if (value !== undefined && !isHttpUrl(value)) {
|
|
@@ -143,4 +147,4 @@ function parseConfig(env) {
|
|
|
143
147
|
hasAllRequired: exports.REQUIRED_KEYS.every(key => presence[key]),
|
|
144
148
|
};
|
|
145
149
|
}
|
|
146
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
150
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZW52LWNvbmZpZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jb25maWcvZW52LWNvbmZpZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7QUErRkEsd0NBMEJDO0FBNENELGtDQW1FQztBQXhPRCw2QkFBd0I7QUFFeEIsMkNBQXFEO0FBQ3JELDJEQUEyQztBQUMzQyxzQ0FBK0M7QUFDL0MsbURBQXVEO0FBRTFDLFFBQUEsYUFBYSxHQUFHO0lBQzNCLG9CQUFvQjtJQUNwQixtQkFBbUI7SUFDbkIsbUJBQW1CO0lBQ25CLGdCQUFnQjtJQUNoQixXQUFXO0NBQ0gsQ0FBQztBQUlFLFFBQUEsUUFBUSxHQUF5QztJQUM1RCxpQkFBaUIsRUFBRSw2QkFBNkI7SUFDaEQsY0FBYyxFQUFFLDZCQUE2QjtDQUM5QyxDQUFDO0FBRUYsTUFBTSxRQUFRLEdBQUcsQ0FBQyxtQkFBbUIsRUFBRSxnQkFBZ0IsRUFBRSxXQUFXLENBQVUsQ0FBQztBQXlCL0UsTUFBTSxlQUFlLEdBQUcsT0FBTyxDQUFDO0FBQ25CLFFBQUEsd0JBQXdCLEdBQUcsS0FBTSxDQUFDO0FBQ2xDLFFBQUEscUJBQXFCLEdBQUcsTUFBTyxDQUFDO0FBQ2hDLFFBQUEsY0FBYyxHQUFHLFVBQWEsQ0FBQztBQUMvQixRQUFBLCtCQUErQixHQUFHLEdBQUcsQ0FBQztBQUN0QyxRQUFBLDRCQUE0QixHQUFHLEtBQU0sQ0FBQztBQUN0QyxRQUFBLHVCQUF1QixHQUFHLEtBQU0sQ0FBQztBQUNqQyxRQUFBLHdCQUF3QixHQUFHLElBQUssQ0FBQztBQUM5QyxNQUFNLFFBQVEsR0FBRyxLQUFLLENBQUM7QUFDdkIsTUFBTSxvQkFBb0IsR0FBRyxFQUFFLENBQUM7QUFDaEMsTUFBTSxjQUFjLEdBQUcsd0JBQXdCLENBQUM7QUFDaEQsTUFBTSxlQUFlLEdBQUcsT0FBQyxDQUFDLEdBQUcsQ0FBQyxFQUFFLFFBQVEsRUFBRSxVQUFVLEVBQUUsQ0FBQyxDQUFDO0FBRXhELFNBQVMs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
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one piece of state the preload and the CLI have to share.
|
|
3
|
+
*
|
|
4
|
+
* `--require` runs the preload in its own module, before `cli.js` is even loaded, so the SDK it
|
|
5
|
+
* builds has nowhere to go but a module both sides resolve to the same instance. Keeping it here —
|
|
6
|
+
* rather than having the preload arm a signal handler of its own — is what lets the shutdown path
|
|
7
|
+
* WAIT for the flush instead of racing it, and keeps the preload from consuming a signal the CLI
|
|
8
|
+
* has not armed a handler for yet.
|
|
9
|
+
*
|
|
10
|
+
* Nothing OpenTelemetry-specific is imported here, so the npm bin pays only for an empty variable.
|
|
11
|
+
*/
|
|
12
|
+
export interface TracingHandle {
|
|
13
|
+
shutdown(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare function setTracingHandle(sdk: TracingHandle | undefined): void;
|
|
16
|
+
export declare function getTracingHandle(): TracingHandle | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Flushes buffered spans, or resolves immediately when tracing was never armed — the shutdown path
|
|
19
|
+
* should not have to know which. A failing export is swallowed: a dead collector must not turn a
|
|
20
|
+
* clean shutdown into a failed one.
|
|
21
|
+
*
|
|
22
|
+
* No deadline here on purpose: `armShutdown` bounds whatever flush it is given, and it is the only
|
|
23
|
+
* caller. Bounding it twice would say the deadline lives in two places when it does not.
|
|
24
|
+
*/
|
|
25
|
+
export declare function flushTracing(): Promise<void>;
|
|
26
|
+
//# sourceMappingURL=tracing-handle.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The one piece of state the preload and the CLI have to share.
|
|
4
|
+
*
|
|
5
|
+
* `--require` runs the preload in its own module, before `cli.js` is even loaded, so the SDK it
|
|
6
|
+
* builds has nowhere to go but a module both sides resolve to the same instance. Keeping it here —
|
|
7
|
+
* rather than having the preload arm a signal handler of its own — is what lets the shutdown path
|
|
8
|
+
* WAIT for the flush instead of racing it, and keeps the preload from consuming a signal the CLI
|
|
9
|
+
* has not armed a handler for yet.
|
|
10
|
+
*
|
|
11
|
+
* Nothing OpenTelemetry-specific is imported here, so the npm bin pays only for an empty variable.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.setTracingHandle = setTracingHandle;
|
|
15
|
+
exports.getTracingHandle = getTracingHandle;
|
|
16
|
+
exports.flushTracing = flushTracing;
|
|
17
|
+
let handle;
|
|
18
|
+
function setTracingHandle(sdk) {
|
|
19
|
+
handle = sdk;
|
|
20
|
+
}
|
|
21
|
+
function getTracingHandle() {
|
|
22
|
+
return handle;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Flushes buffered spans, or resolves immediately when tracing was never armed — the shutdown path
|
|
26
|
+
* should not have to know which. A failing export is swallowed: a dead collector must not turn a
|
|
27
|
+
* clean shutdown into a failed one.
|
|
28
|
+
*
|
|
29
|
+
* No deadline here on purpose: `armShutdown` bounds whatever flush it is given, and it is the only
|
|
30
|
+
* caller. Bounding it twice would say the deadline lives in two places when it does not.
|
|
31
|
+
*/
|
|
32
|
+
async function flushTracing() {
|
|
33
|
+
try {
|
|
34
|
+
await handle?.shutdown();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* istanbul ignore next — nothing to do about it, and it must not change the exit code. */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHJhY2luZy1oYW5kbGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdHJhY2luZy1oYW5kbGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Ozs7Ozs7O0dBVUc7O0FBUUgsNENBRUM7QUFFRCw0Q0FFQztBQVVELG9DQU1DO0FBeEJELElBQUksTUFBaUMsQ0FBQztBQUV0QyxTQUFnQixnQkFBZ0IsQ0FBQyxHQUE4QjtJQUM3RCxNQUFNLEdBQUcsR0FBRyxDQUFDO0FBQ2YsQ0FBQztBQUVELFNBQWdCLGdCQUFnQjtJQUM5QixPQUFPLE1BQU0sQ0FBQztBQUNoQixDQUFDO0FBRUQ7Ozs7Ozs7R0FPRztBQUNJLEtBQUssVUFBVSxZQUFZO0lBQ2hDLElBQUksQ0FBQztRQUNILE1BQU0sTUFBTSxFQUFFLFFBQVEsRUFBRSxDQUFDO0lBQzNCLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCwwRkFBMEY7SUFDNUYsQ0FBQztBQUNILENBQUMifQ==
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// The `--require` entry point of the Docker image (see the Dockerfile's ENTRYPOINT). Separate from
|
|
7
|
+
// `tracing.ts` on purpose: importing the setup must never arm an SDK, only calling it should.
|
|
8
|
+
//
|
|
9
|
+
// The SDK is handed to `tracing-handle` rather than wired to a signal here. The CLI arms the only
|
|
10
|
+
// termination handler, and it flushes through that handle — so a signal arriving before the CLI is
|
|
11
|
+
// up cannot be swallowed by a listener that does not terminate anything.
|
|
12
|
+
const console_logger_1 = __importDefault(require("./adapters/console-logger"));
|
|
13
|
+
const tracing_1 = __importDefault(require("./tracing"));
|
|
14
|
+
const tracing_handle_1 = require("./tracing-handle");
|
|
15
|
+
// Nothing here may throw. A `--require` runs before the entry point, so an exception aborts the
|
|
16
|
+
// process before the BFF exists at all — a dead container in exchange for optional, best-effort
|
|
17
|
+
// telemetry. `initTracing` degrades on its own for the cases it knows about; this catches the rest,
|
|
18
|
+
// including whatever a future SDK version decides to throw from `start()`.
|
|
19
|
+
try {
|
|
20
|
+
(0, tracing_handle_1.setTracingHandle)((0, tracing_1.default)());
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
(0, console_logger_1.default)()('Warn', 'OpenTelemetry failed to initialise, starting untraced', {
|
|
24
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHJhY2luZy1wcmVsb2FkLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL3RyYWNpbmctcHJlbG9hZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQUFBLG1HQUFtRztBQUNuRyw4RkFBOEY7QUFDOUYsRUFBRTtBQUNGLGtHQUFrRztBQUNsRyxtR0FBbUc7QUFDbkcseUVBQXlFO0FBQ3pFLCtFQUE0RDtBQUM1RCx3REFBb0M7QUFDcEMscURBQW9EO0FBRXBELGdHQUFnRztBQUNoRyxnR0FBZ0c7QUFDaEcsb0dBQW9HO0FBQ3BHLDJFQUEyRTtBQUMzRSxJQUFJLENBQUM7SUFDSCxJQUFBLGlDQUFnQixFQUFDLElBQUEsaUJBQVcsR0FBRSxDQUFDLENBQUM7QUFDbEMsQ0FBQztBQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7SUFDZixJQUFBLHdCQUFtQixHQUFFLENBQUMsTUFBTSxFQUFFLHVEQUF1RCxFQUFFO1FBQ3JGLE1BQU0sRUFBRSxLQUFLLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDO0tBQy9ELENBQUMsQ0FBQztBQUNMLENBQUMifQ==
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Logger } from './ports/logger-port';
|
|
2
|
+
/**
|
|
3
|
+
* The OpenTelemetry setup. `tracing-preload.ts` is what actually runs it, via `--require` before
|
|
4
|
+
* cli.js so the auto-instrumentation patches land before anything the app imports; keeping the two
|
|
5
|
+
* apart is what lets this module be imported by a test without arming an SDK.
|
|
6
|
+
*
|
|
7
|
+
* The SDK is initialised only when `OTEL_EXPORTER_OTLP_ENDPOINT` is set, so an install that never
|
|
8
|
+
* opted into APM pays nothing. Neither ending the process nor flushing on the way out is this
|
|
9
|
+
* module's business: it hands the SDK back, the preload parks it in `tracing-handle`, and the
|
|
10
|
+
* shutdown path flushes through that. Arming a signal handler here would consume a signal the CLI
|
|
11
|
+
* has not armed its own handler for yet, and nothing would then terminate the process.
|
|
12
|
+
* All configuration goes through the standard OTel environment variables:
|
|
13
|
+
*
|
|
14
|
+
* OTEL_EXPORTER_OTLP_ENDPOINT OTLP receiver (e.g. http://localhost:4318)
|
|
15
|
+
* OTEL_SERVICE_NAME default: forestadmin-agent-bff
|
|
16
|
+
* OTEL_SDK_DISABLED set to "true" to force-disable
|
|
17
|
+
* OTEL_RESOURCE_ATTRIBUTES e.g. deployment.environment=production
|
|
18
|
+
* OTEL_METRICS_EXPORTER default: none (the SDK's own default is otlp)
|
|
19
|
+
* OTEL_LOGS_EXPORTER default: none (the SDK's own default is otlp)
|
|
20
|
+
*
|
|
21
|
+
* The OTel packages are installed only into the Docker image's isolated deps (see
|
|
22
|
+
* `packages/agent-bff/docker/`), never shipped to npm consumers of the CLI — hence the dynamic
|
|
23
|
+
* require behind a seam, and the warning rather than a crash when they are absent.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DEFAULT_SERVICE_NAME = "forestadmin-agent-bff";
|
|
26
|
+
export interface OtelSdk {
|
|
27
|
+
start(): void;
|
|
28
|
+
shutdown(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export interface OtelModules {
|
|
31
|
+
NodeSDK: new (options: Record<string, unknown>) => OtelSdk;
|
|
32
|
+
getNodeAutoInstrumentations: () => unknown;
|
|
33
|
+
}
|
|
34
|
+
export interface TracingOptions {
|
|
35
|
+
/**
|
|
36
|
+
* The environment OUR decisions read: whether to arm at all, and what to name the service when
|
|
37
|
+
* nothing else does. It stops there — the SDK reads the real `process.env` for everything about
|
|
38
|
+
* the export itself, so a test injecting `env` cannot assert where spans go. Identical objects in
|
|
39
|
+
* production, so this is a limit on what the seam can test, not a behaviour.
|
|
40
|
+
*/
|
|
41
|
+
env?: NodeJS.ProcessEnv;
|
|
42
|
+
logger?: Logger;
|
|
43
|
+
/** The dynamic require, as a seam: the packages exist only in the Docker image. */
|
|
44
|
+
load?: () => OtelModules | undefined;
|
|
45
|
+
}
|
|
46
|
+
/** The packages this image installs for APM, and the export it takes from each. */
|
|
47
|
+
export declare const OTEL_MODULE_IDS: {
|
|
48
|
+
readonly sdk: "@opentelemetry/sdk-node";
|
|
49
|
+
readonly instrumentations: "@opentelemetry/auto-instrumentations-node";
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The module resolution, as a seam. These packages are absent everywhere except the Docker image,
|
|
53
|
+
* so the real `require` can only ever take the failure branch here — passing a loader is what lets
|
|
54
|
+
* the success branch, and the package ids it asks for, be exercised at all. A typo in one of them
|
|
55
|
+
* would otherwise surface as an untraced image and nothing else.
|
|
56
|
+
*/
|
|
57
|
+
export type ModuleLoader = (id: string) => Record<string, unknown>;
|
|
58
|
+
export declare function loadOtelModules(load?: ModuleLoader): OtelModules | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* The service name the environment already carries, by the spec's precedence: `OTEL_SERVICE_NAME`
|
|
61
|
+
* wins, then a `service.name` entry in the comma-separated `OTEL_RESOURCE_ATTRIBUTES` list.
|
|
62
|
+
* Undefined when neither names one, which is the only case where our own default should apply.
|
|
63
|
+
*/
|
|
64
|
+
export declare function serviceNameFromEnv(env: NodeJS.ProcessEnv): string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Strips the credentials out of the endpoint before it reaches the logs. `https://user:token@host`
|
|
67
|
+
* is a legitimate way to reach a collector behind basic auth, and container logs are the last place
|
|
68
|
+
* that token should end up — this package does not echo a secret anywhere else either.
|
|
69
|
+
*
|
|
70
|
+
* The query string goes with them, unread: collectors that take their key as `?api_key=` exist, and
|
|
71
|
+
* telling them apart from a harmless parameter means knowing every vendor's spelling. Nothing about
|
|
72
|
+
* the destination is lost — the host and path are what the line is for.
|
|
73
|
+
*
|
|
74
|
+
* An endpoint that does not parse is dropped from the log entirely rather than passed through: it
|
|
75
|
+
* cannot be redacted, so it cannot be shown. The SDK will fail on it soon enough on its own.
|
|
76
|
+
*/
|
|
77
|
+
export declare function redactEndpoint(endpoint: string): string | undefined;
|
|
78
|
+
export default function initTracing(options?: TracingOptions): OtelSdk | undefined;
|
|
79
|
+
//# sourceMappingURL=tracing.d.ts.map
|
package/dist/tracing.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OTEL_MODULE_IDS = exports.DEFAULT_SERVICE_NAME = void 0;
|
|
7
|
+
exports.loadOtelModules = loadOtelModules;
|
|
8
|
+
exports.serviceNameFromEnv = serviceNameFromEnv;
|
|
9
|
+
exports.redactEndpoint = redactEndpoint;
|
|
10
|
+
exports.default = initTracing;
|
|
11
|
+
const console_logger_1 = __importDefault(require("./adapters/console-logger"));
|
|
12
|
+
/**
|
|
13
|
+
* The OpenTelemetry setup. `tracing-preload.ts` is what actually runs it, via `--require` before
|
|
14
|
+
* cli.js so the auto-instrumentation patches land before anything the app imports; keeping the two
|
|
15
|
+
* apart is what lets this module be imported by a test without arming an SDK.
|
|
16
|
+
*
|
|
17
|
+
* The SDK is initialised only when `OTEL_EXPORTER_OTLP_ENDPOINT` is set, so an install that never
|
|
18
|
+
* opted into APM pays nothing. Neither ending the process nor flushing on the way out is this
|
|
19
|
+
* module's business: it hands the SDK back, the preload parks it in `tracing-handle`, and the
|
|
20
|
+
* shutdown path flushes through that. Arming a signal handler here would consume a signal the CLI
|
|
21
|
+
* has not armed its own handler for yet, and nothing would then terminate the process.
|
|
22
|
+
* All configuration goes through the standard OTel environment variables:
|
|
23
|
+
*
|
|
24
|
+
* OTEL_EXPORTER_OTLP_ENDPOINT OTLP receiver (e.g. http://localhost:4318)
|
|
25
|
+
* OTEL_SERVICE_NAME default: forestadmin-agent-bff
|
|
26
|
+
* OTEL_SDK_DISABLED set to "true" to force-disable
|
|
27
|
+
* OTEL_RESOURCE_ATTRIBUTES e.g. deployment.environment=production
|
|
28
|
+
* OTEL_METRICS_EXPORTER default: none (the SDK's own default is otlp)
|
|
29
|
+
* OTEL_LOGS_EXPORTER default: none (the SDK's own default is otlp)
|
|
30
|
+
*
|
|
31
|
+
* The OTel packages are installed only into the Docker image's isolated deps (see
|
|
32
|
+
* `packages/agent-bff/docker/`), never shipped to npm consumers of the CLI — hence the dynamic
|
|
33
|
+
* require behind a seam, and the warning rather than a crash when they are absent.
|
|
34
|
+
*/
|
|
35
|
+
exports.DEFAULT_SERVICE_NAME = 'forestadmin-agent-bff';
|
|
36
|
+
/** The packages this image installs for APM, and the export it takes from each. */
|
|
37
|
+
exports.OTEL_MODULE_IDS = {
|
|
38
|
+
sdk: '@opentelemetry/sdk-node',
|
|
39
|
+
instrumentations: '@opentelemetry/auto-instrumentations-node',
|
|
40
|
+
};
|
|
41
|
+
function loadOtelModules(load = require) {
|
|
42
|
+
try {
|
|
43
|
+
const modules = {
|
|
44
|
+
NodeSDK: load(exports.OTEL_MODULE_IDS.sdk).NodeSDK,
|
|
45
|
+
getNodeAutoInstrumentations: load(exports.OTEL_MODULE_IDS.instrumentations)
|
|
46
|
+
.getNodeAutoInstrumentations,
|
|
47
|
+
};
|
|
48
|
+
// Resolving is not the same as finding what we came for. A package that loads but no longer
|
|
49
|
+
// exports the name we read — a rename on a version bump — would otherwise hand back a truthy
|
|
50
|
+
// object of undefined members, skip the "packages not available" branch, and throw a
|
|
51
|
+
// TypeError inside a `--require`, before the entry point runs at all. That is a dead container
|
|
52
|
+
// for an APM layer that is supposed to degrade to a warning.
|
|
53
|
+
if (Object.values(modules).some(exported => typeof exported !== 'function'))
|
|
54
|
+
return undefined;
|
|
55
|
+
return modules;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The OTel specification defines its boolean environment variables as case-insensitive, and this
|
|
63
|
+
* one is the kill switch: reading `TRUE` as "not disabled" would leave tracing running for someone
|
|
64
|
+
* who just asked for it to stop.
|
|
65
|
+
*/
|
|
66
|
+
function isDisabled(raw) {
|
|
67
|
+
return raw?.trim().toLowerCase() === 'true';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The service name the environment already carries, by the spec's precedence: `OTEL_SERVICE_NAME`
|
|
71
|
+
* wins, then a `service.name` entry in the comma-separated `OTEL_RESOURCE_ATTRIBUTES` list.
|
|
72
|
+
* Undefined when neither names one, which is the only case where our own default should apply.
|
|
73
|
+
*/
|
|
74
|
+
function serviceNameFromEnv(env) {
|
|
75
|
+
const explicit = env.OTEL_SERVICE_NAME?.trim();
|
|
76
|
+
if (explicit)
|
|
77
|
+
return explicit;
|
|
78
|
+
return env.OTEL_RESOURCE_ATTRIBUTES?.split(',')
|
|
79
|
+
.map(entry => entry.split('='))
|
|
80
|
+
.filter(([key, value]) => key?.trim() === 'service.name' && value?.trim())
|
|
81
|
+
.map(([, value]) => value.trim())
|
|
82
|
+
.pop();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Strips the credentials out of the endpoint before it reaches the logs. `https://user:token@host`
|
|
86
|
+
* is a legitimate way to reach a collector behind basic auth, and container logs are the last place
|
|
87
|
+
* that token should end up — this package does not echo a secret anywhere else either.
|
|
88
|
+
*
|
|
89
|
+
* The query string goes with them, unread: collectors that take their key as `?api_key=` exist, and
|
|
90
|
+
* telling them apart from a harmless parameter means knowing every vendor's spelling. Nothing about
|
|
91
|
+
* the destination is lost — the host and path are what the line is for.
|
|
92
|
+
*
|
|
93
|
+
* An endpoint that does not parse is dropped from the log entirely rather than passed through: it
|
|
94
|
+
* cannot be redacted, so it cannot be shown. The SDK will fail on it soon enough on its own.
|
|
95
|
+
*/
|
|
96
|
+
function redactEndpoint(endpoint) {
|
|
97
|
+
try {
|
|
98
|
+
const url = new URL(endpoint);
|
|
99
|
+
if (!url.username && !url.password && !url.search)
|
|
100
|
+
return endpoint;
|
|
101
|
+
url.username = '';
|
|
102
|
+
url.password = '';
|
|
103
|
+
url.search = '';
|
|
104
|
+
return url.toString();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function initTracing(options = {}) {
|
|
111
|
+
const { env = process.env, logger = (0, console_logger_1.default)(), load = loadOtelModules } = options;
|
|
112
|
+
// Any of the three standard ways to say "export traces somewhere" turns tracing on. The per-signal
|
|
113
|
+
// endpoint counts as much as the generic one, and OTEL_TRACES_EXPORTER counts on its own —
|
|
114
|
+
// requiring an OTLP collector before `console` does anything would be nonsense for an exporter
|
|
115
|
+
// that writes to stdout, and it is the obvious first thing to reach for when debugging.
|
|
116
|
+
//
|
|
117
|
+
// A blank value counts as unset throughout, not as "export to the OTel default": `env_file` hands
|
|
118
|
+
// an empty string through for a variable left blank in a `.env`, and arming the SDK against
|
|
119
|
+
// localhost:4318 is not what leaving the line empty asks for.
|
|
120
|
+
const endpoint = env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim() || env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
|
|
121
|
+
const requestedExporter = env.OTEL_TRACES_EXPORTER?.trim().toLowerCase();
|
|
122
|
+
if ((!endpoint && !requestedExporter) || isDisabled(env.OTEL_SDK_DISABLED))
|
|
123
|
+
return undefined;
|
|
124
|
+
const modules = load();
|
|
125
|
+
if (!modules) {
|
|
126
|
+
logger('Warn', 'OpenTelemetry packages not available, skipping APM initialisation');
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const { NodeSDK, getNodeAutoInstrumentations } = modules;
|
|
130
|
+
// Passing `serviceName` unconditionally would override OTEL_RESOURCE_ATTRIBUTES=service.name=...,
|
|
131
|
+
// which is a documented knob — someone setting only that would silently get our default. So we
|
|
132
|
+
// fill it in only when the environment names no service at all, and otherwise leave the SDK to
|
|
133
|
+
// apply the spec's own precedence (OTEL_SERVICE_NAME first, then the resource attribute).
|
|
134
|
+
const named = serviceNameFromEnv(env);
|
|
135
|
+
// NodeSDK reads OTEL_METRICS_EXPORTER and OTEL_LOGS_EXPORTER itself, and an unset one does not
|
|
136
|
+
// mean "off" — it means otlp. So arming traces alone also starts a metric reader and a log
|
|
137
|
+
// processor aimed at the OTLP default, http://localhost:4318. Under OTEL_TRACES_EXPORTER=console,
|
|
138
|
+
// which asks for no collector at all, that is a POST every minute to a port nobody named; where
|
|
139
|
+
// something else on the host does listen there, it is telemetry that install never opted into.
|
|
140
|
+
// This module arms tracing. The other two signals stay opt-in, through their own variable, which
|
|
141
|
+
// still works: we only fill in what the environment left blank.
|
|
142
|
+
env.OTEL_METRICS_EXPORTER = env.OTEL_METRICS_EXPORTER?.trim() || 'none';
|
|
143
|
+
env.OTEL_LOGS_EXPORTER = env.OTEL_LOGS_EXPORTER?.trim() || 'none';
|
|
144
|
+
// No `traceExporter`, deliberately. Passing one puts NodeSDK on its manual-configuration path,
|
|
145
|
+
// where it stops reading the environment — which is how OTEL_TRACES_EXPORTER, then
|
|
146
|
+
// OTEL_EXPORTER_OTLP_PROTOCOL, then the per-signal endpoint each turned out to be silently
|
|
147
|
+
// ignored, one review round after another. They were three symptoms of doing the SDK's job.
|
|
148
|
+
//
|
|
149
|
+
// sdk-node depends on every OTLP exporter and on the Zipkin one, so all of them are in the image
|
|
150
|
+
// already; leaving the choice to the SDK costs nothing and makes the whole standard surface work,
|
|
151
|
+
// protocol and per-signal overrides included. Our only decisions are whether to arm at all, and
|
|
152
|
+
// the service name when nothing else supplies one.
|
|
153
|
+
const sdk = new NodeSDK({
|
|
154
|
+
...(named ? {} : { serviceName: exports.DEFAULT_SERVICE_NAME }),
|
|
155
|
+
instrumentations: [getNodeAutoInstrumentations()],
|
|
156
|
+
});
|
|
157
|
+
sdk.start();
|
|
158
|
+
// What was ASKED for, not what the SDK settled on — we no longer choose, so claiming a
|
|
159
|
+
// destination we did not pick would be inventing one. The endpoint is dropped when an exporter
|
|
160
|
+
// was named instead, since it would read as a promise this line cannot keep.
|
|
161
|
+
logger('Info', 'OpenTelemetry tracing enabled', {
|
|
162
|
+
serviceName: named ?? exports.DEFAULT_SERVICE_NAME,
|
|
163
|
+
...(requestedExporter ? { exporter: requestedExporter } : {}),
|
|
164
|
+
...(endpoint && !requestedExporter ? { endpoint: redactEndpoint(endpoint) } : {}),
|
|
165
|
+
});
|
|
166
|
+
return sdk;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHJhY2luZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy90cmFjaW5nLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7OztBQW1FQSwwQ0FtQkM7QUFnQkQsZ0RBVUM7QUFjRCx3Q0FjQztBQUVELDhCQXFFQztBQWpORCwrRUFBNEQ7QUFFNUQ7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FzQkc7QUFFVSxRQUFBLG9CQUFvQixHQUFHLHVCQUF1QixDQUFDO0FBeUI1RCxtRkFBbUY7QUFDdEUsUUFBQSxlQUFlLEdBQUc7SUFDN0IsR0FBRyxFQUFFLHlCQUF5QjtJQUM5QixnQkFBZ0IsRUFBRSwyQ0FBMkM7Q0FDckQsQ0FBQztBQVVYLFNBQWdCLGVBQWUsQ0FBQyxPQUFxQixPQUFPO0lBQzFELElBQUksQ0FBQztRQUNILE1BQU0sT0FBTyxHQUFHO1lBQ2QsT0FBTyxFQUFFLElBQUksQ0FBQyx1QkFBZSxDQUFDLEdBQUcsQ0FBQyxDQUFDLE9BQU87WUFDMUMsMkJBQTJCLEVBQUUsSUFBSSxDQUFDLHVCQUFlLENBQUMsZ0JBQWdCLENBQUM7aUJBQ2hFLDJCQUEyQjtTQUMvQixDQUFDO1FBRUYsNEZBQTRGO1FBQzVGLDZGQUE2RjtRQUM3RixxRkFBcUY7UUFDckYsK0ZBQStGO1FBQy9GLDZEQUE2RDtRQUM3RCxJQUFJLE1BQU0sQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUMsT0FBTyxRQUFRLEtBQUssVUFBVSxDQUFDO1lBQUUsT0FBTyxTQUFTLENBQUM7UUFFOUYsT0FBTyxPQUFzQixDQUFDO0lBQ2hDLENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0FBQ0gsQ0FBQztBQUVEOzs7O0dBSUc7QUFDSCxTQUFTLFVBQVUsQ0FBQyxHQUF1QjtJQUN6QyxPQUFPLEdBQUcsRUFBRSxJQUFJLEVBQUUsQ0FBQyxXQUFXLEVBQUUsS0FBSyxNQUFN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
|