@lntrn/otel 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First release (LAN-365).
6
+ - **`node --import @lntrn/otel/register`** starts a stock `NodeSDK` from
7
+ `LNTRN_*` / `OTEL_*`:
8
+ - it registers the ES module hook;
9
+ - it loads `@opentelemetry/auto-instrumentations-node` when installed;
10
+ - it flushes on exit and on `SIGTERM`/`SIGINT`.
11
+ - **`setup(options)`** does the same from code. Targets:
12
+ - Lantern's edge, with Basic auth from an edge credential;
13
+ - the client ingest, with a device token through `@lntrn/device/otel`'s
14
+ `TokenCache`;
15
+ - an in-cluster gateway;
16
+ - plain `OTEL_EXPORTER_OTLP_*`.
17
+ - **`lanternDetector()`** sets `service.namespace` and
18
+ `deployment.environment.name` from `LNTRN_NAMESPACE` / `LNTRN_ENVIRONMENT`.
package/README.md CHANGED
@@ -1,3 +1,122 @@
1
- # Temporary Holding Version
1
+ # @lntrn/otel
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Lantern's add-on for the OpenTelemetry Node SDK.
4
+
5
+ ## Already on OpenTelemetry? Add this
6
+
7
+ ```bash
8
+ npm install @lntrn/otel @opentelemetry/api @opentelemetry/auto-instrumentations-node
9
+ ```
10
+
11
+ ```bash
12
+ export OTEL_SERVICE_NAME=checkout-api
13
+ export LNTRN_EDGE_USER=<org>.<id> LNTRN_EDGE_PASSWORD=… # an edge credential
14
+ export LNTRN_NAMESPACE=checkout LNTRN_ENVIRONMENT=prod
15
+ node --import @lntrn/otel/register server.js
16
+ ```
17
+
18
+ That replaces your `instrumentation.mjs`. `@lntrn/otel/register` starts a
19
+ stock `NodeSDK` before the app loads:
20
+ - the stock OTLP/HTTP protobuf exporters for traces, metrics and logs;
21
+ - `@opentelemetry/auto-instrumentations-node` when it is installed, with
22
+ `fs` off;
23
+ - OpenTelemetry's ES module hook, so ESM libraries (pino, express, …) are
24
+ instrumented too;
25
+ - a flush before the process exits, when the event loop empties and on
26
+ `SIGTERM`/`SIGINT`.
27
+
28
+ Your code keeps using `@opentelemetry/api` as before.
29
+
30
+ In a container: `CMD ["node", "--import", "@lntrn/otel/register", "server.js"]`,
31
+ or `NODE_OPTIONS="--import @lntrn/otel/register"`.
32
+
33
+ Already starting `NodeSDK` yourself? Call `setup()` instead, with the same
34
+ options plus your own:
35
+
36
+ ```js
37
+ // instrumentation.mjs, loaded with --import
38
+ import { register } from "node:module";
39
+ import { setup } from "@lntrn/otel";
40
+ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
41
+
42
+ register("@opentelemetry/instrumentation/hook.mjs", import.meta.url);
43
+ const lantern = setup({
44
+ serviceName: "checkout-api",
45
+ instrumentations: [getNodeAutoInstrumentations()],
46
+ sdk: { /* sampler, views, … anything else NodeSDK takes */ },
47
+ });
48
+ process.once("SIGTERM", () => lantern.shutdown().finally(() => process.exit(0)));
49
+ ```
50
+
51
+ Requires Node 22.15 or newer, as `@opentelemetry/sdk-node` 0.300 does.
52
+ Tested with `sdk-node` 0.300.0 and `@opentelemetry/api` 1.10.0.
53
+
54
+ ## Targets
55
+
56
+ `setup()` picks the first one configured, unless `target` (or `LNTRN_TARGET`)
57
+ names one:
58
+
59
+ | Target | When | Sends to | Auth |
60
+ |---|---|---|---|
61
+ | `edge` | `LNTRN_EDGE_USER` + `LNTRN_EDGE_PASSWORD` | `https://otlp.lntrn.dev` | HTTP Basic, the edge credential |
62
+ | `device` | `token` or `LNTRN_DEVICE_TOKEN` | `https://in.lntrn.dev` | `Bearer <device token>` |
63
+ | `gateway` | `LNTRN_ENDPOINT` with no credential | that endpoint, e.g. an in-cluster collector | none |
64
+ | `otel` | `OTEL_EXPORTER_OTLP_ENDPOINT` (or a signal's own) | wherever `OTEL_EXPORTER_OTLP_*` say: plain `NodeSDK` | `OTEL_EXPORTER_OTLP_HEADERS` |
65
+ | `none` | nothing configured, or `OTEL_SDK_DISABLED=true` | nowhere. Spans still get ids, so log lines still carry them | |
66
+
67
+ Details:
68
+ - `LNTRN_ENDPOINT` (`endpoint`) replaces the edge's or the ingest's address,
69
+ e.g. `https://forwarder.otlp.lntrn.dev`.
70
+ - The paths `/v1/traces`, `/v1/metrics` and `/v1/logs` are appended.
71
+ - Lantern's endpoints are OTLP/HTTP only. There is no gRPC.
72
+
73
+ ### Node processes on devices
74
+
75
+ A process running on hardware you don't control can't hold an edge credential.
76
+ Examples: an emulation node, a kiosk, a desktop app's main process. It sends
77
+ to the client ingest with a device token instead:
78
+
79
+ ```js
80
+ import { setup } from "@lntrn/otel";
81
+
82
+ setup({
83
+ serviceName: "emulation-node",
84
+ // Called only when the cached token is due for renewal: a minute before exp.
85
+ token: () => fetchTokenFromYourBackend(), // or mintToken({...}) from @lntrn/device/server, where the key lives
86
+ });
87
+ ```
88
+
89
+ The token cache is `@lntrn/device/otel`'s `TokenCache`, the same one the JS
90
+ device add-on uses. Concurrent exports share one fetch, and a failed fetch
91
+ keeps the old token while it is still valid. The ingest files the records
92
+ under the token's project.
93
+
94
+ ## Resource
95
+
96
+ | Attribute | From |
97
+ |---|---|
98
+ | `service.name` | `serviceName`; `OTEL_SERVICE_NAME` wins |
99
+ | `service.version` | `serviceVersion` |
100
+ | `service.namespace` | `namespace` or `LNTRN_NAMESPACE` |
101
+ | `deployment.environment.name` | `environment` or `LNTRN_ENVIRONMENT` |
102
+ | anything | `attributes`; `OTEL_RESOURCE_ATTRIBUTES` wins |
103
+
104
+ `lanternDetector()` is the namespace/environment part as a `ResourceDetector`,
105
+ for a `NodeSDK` you configure entirely yourself.
106
+
107
+ ## Turning a signal off
108
+
109
+ `OTEL_TRACES_EXPORTER`, `OTEL_METRICS_EXPORTER` or `OTEL_LOGS_EXPORTER` set to
110
+ `none` (or `disableTraces`/`disableMetrics`/`disableLogs`) leaves that signal
111
+ without an exporter. In Kubernetes, with Lantern's agent collecting container
112
+ output, set `OTEL_LOGS_EXPORTER=none`. pino's OpenTelemetry instrumentation
113
+ still writes `trace_id` and `span_id` into every stdout line.
114
+
115
+ `LNTRN_INSTRUMENTATIONS=none` stops `register` from loading the
116
+ auto-instrumentations.
117
+
118
+ ## Releasing
119
+
120
+ Bump `"version"` in `package.json`, add the CHANGELOG entry, and merge to
121
+ main. CI (`.xeonr-ci.yaml`, app `lntrn-otel-node`) tests, builds and publishes
122
+ any version the registry hasn't got, and tags it `node-vX.Y.Z`.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `@lntrn/otel`: Lantern's add-on for the OpenTelemetry Node SDK.
3
+ *
4
+ * It does not replace the SDK. `setup()` starts a stock `NodeSDK` with the
5
+ * stock OTLP/HTTP protobuf exporters and adds only what Lantern needs: where
6
+ * to send, how to authenticate, and the resource attributes Lantern groups
7
+ * by. Most services don't call it at all; they start with
8
+ * `node --import @lntrn/otel/register app.js` and configure it with `LNTRN_*`
9
+ * variables.
10
+ */
11
+ import { NodeSDK, type NodeSDKConfiguration } from "@opentelemetry/sdk-node";
12
+ import { type ResourceDetector } from "@opentelemetry/resources";
13
+ import type { Attributes } from "@opentelemetry/api";
14
+ import { type Target, type TargetOptions } from "./target.ts";
15
+ export { EDGE_URL, INGEST_URL, resolveTarget } from "./target.ts";
16
+ export type { ResolvedTarget, Target, TargetOptions } from "./target.ts";
17
+ export { TokenCache, deviceTokenHeaders } from "@lntrn/device/otel";
18
+ export interface LanternOptions extends TargetOptions {
19
+ /** `service.name`. `OTEL_SERVICE_NAME`, when set, wins, as in every SDK. */
20
+ serviceName?: string;
21
+ /** `service.version`: the release, from your CI. */
22
+ serviceVersion?: string;
23
+ /** `service.namespace` (`LNTRN_NAMESPACE`). */
24
+ namespace?: string;
25
+ /** `deployment.environment.name` (`LNTRN_ENVIRONMENT`). */
26
+ environment?: string;
27
+ /** More resource attributes. `OTEL_RESOURCE_ATTRIBUTES` wins over these and the fields above. */
28
+ attributes?: Attributes;
29
+ /**
30
+ * Leave a signal without an exporter. `OTEL_TRACES_EXPORTER`,
31
+ * `OTEL_METRICS_EXPORTER` or `OTEL_LOGS_EXPORTER` set to `none` does the
32
+ * same: in Kubernetes, with Lantern's agent collecting stdout, set
33
+ * `OTEL_LOGS_EXPORTER=none` and keep logging to stdout.
34
+ */
35
+ disableTraces?: boolean;
36
+ disableMetrics?: boolean;
37
+ disableLogs?: boolean;
38
+ /** How often metrics are exported, in ms. Default `OTEL_METRIC_EXPORT_INTERVAL`, then 30000. */
39
+ metricInterval?: number;
40
+ /** Instrumentations to register, e.g. `getNodeAutoInstrumentations()`. */
41
+ instrumentations?: NodeSDKConfiguration["instrumentations"];
42
+ /**
43
+ * Anything else for `NodeSDK` (sampler, views, propagator, …). It is
44
+ * applied last, so it can override the exporters this add-on builds.
45
+ */
46
+ sdk?: Partial<NodeSDKConfiguration>;
47
+ }
48
+ /** What `setup()` started. */
49
+ export interface Lantern {
50
+ target: Target;
51
+ sdk: NodeSDK;
52
+ /** Flushes and stops every provider. Safe to call more than once. */
53
+ shutdown(): Promise<void>;
54
+ }
55
+ /**
56
+ * Starts a stock `NodeSDK` that sends to Lantern, and registers it as the
57
+ * global OpenTelemetry. Call it before the code it instruments is imported
58
+ * (which `@lntrn/otel/register` does for you), and `shutdown()` before the
59
+ * process exits, or the last batch is lost.
60
+ */
61
+ export declare function setup(options?: LanternOptions, env?: Record<string, string | undefined>): Lantern;
62
+ /**
63
+ * A resource detector for `service.namespace` and
64
+ * `deployment.environment.name`, from the given values or `LNTRN_NAMESPACE`
65
+ * and `LNTRN_ENVIRONMENT`. For a `NodeSDK` you configure yourself:
66
+ *
67
+ * ```ts
68
+ * new NodeSDK({ resourceDetectors: [lanternDetector(), envDetector, processDetector, hostDetector] });
69
+ * ```
70
+ *
71
+ * Put `envDetector` after it, so `OTEL_RESOURCE_ATTRIBUTES` still wins.
72
+ */
73
+ export declare function lanternDetector(o?: {
74
+ namespace?: string;
75
+ environment?: string;
76
+ }, env?: Record<string, string | undefined>): ResourceDetector;
package/dist/index.js ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `@lntrn/otel`: Lantern's add-on for the OpenTelemetry Node SDK.
3
+ *
4
+ * It does not replace the SDK. `setup()` starts a stock `NodeSDK` with the
5
+ * stock OTLP/HTTP protobuf exporters and adds only what Lantern needs: where
6
+ * to send, how to authenticate, and the resource attributes Lantern groups
7
+ * by. Most services don't call it at all; they start with
8
+ * `node --import @lntrn/otel/register app.js` and configure it with `LNTRN_*`
9
+ * variables.
10
+ */
11
+ import { NodeSDK } from "@opentelemetry/sdk-node";
12
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
13
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
14
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
15
+ import { BatchSpanProcessor, NoopSpanProcessor } from "@opentelemetry/sdk-trace";
16
+ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
17
+ import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
18
+ import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";
19
+ import { resolveTarget } from "./target.js";
20
+ export { EDGE_URL, INGEST_URL, resolveTarget } from "./target.js";
21
+ export { TokenCache, deviceTokenHeaders } from "@lntrn/device/otel";
22
+ /**
23
+ * Starts a stock `NodeSDK` that sends to Lantern, and registers it as the
24
+ * global OpenTelemetry. Call it before the code it instruments is imported
25
+ * (which `@lntrn/otel/register` does for you), and `shutdown()` before the
26
+ * process exits, or the last batch is lost.
27
+ */
28
+ export function setup(options = {}, env = process.env) {
29
+ const resolved = resolveTarget(options, env);
30
+ const config = {
31
+ resource: defaultResource().merge(resourceFromAttributes(resourceAttributes(options, env))),
32
+ instrumentations: options.instrumentations ?? [],
33
+ };
34
+ if (resolved.target === "none") {
35
+ // Providers with nothing behind them: spans still get ids, so logs
36
+ // still carry them, and nothing is sent anywhere.
37
+ config.spanProcessors = [new NoopSpanProcessor()];
38
+ config.metricReaders = [];
39
+ config.logRecordProcessors = [];
40
+ }
41
+ else if (resolved.url) {
42
+ const { url, headers } = resolved;
43
+ const off = (flag, name) => flag || env[name]?.trim().toLowerCase() === "none";
44
+ config.spanProcessors = off(options.disableTraces, "OTEL_TRACES_EXPORTER")
45
+ ? [new NoopSpanProcessor()]
46
+ : [new BatchSpanProcessor({ exporter: new OTLPTraceExporter({ url: `${url}/v1/traces`, headers }) })];
47
+ config.metricReaders = off(options.disableMetrics, "OTEL_METRICS_EXPORTER")
48
+ ? []
49
+ : [
50
+ new PeriodicExportingMetricReader({
51
+ exporter: new OTLPMetricExporter({ url: `${url}/v1/metrics`, headers }),
52
+ exportIntervalMillis: options.metricInterval ?? (Number(env.OTEL_METRIC_EXPORT_INTERVAL) || 30_000),
53
+ }),
54
+ ];
55
+ config.logRecordProcessors = off(options.disableLogs, "OTEL_LOGS_EXPORTER")
56
+ ? []
57
+ : [new BatchLogRecordProcessor({ exporter: new OTLPLogExporter({ url: `${url}/v1/logs`, headers }) })];
58
+ }
59
+ // target "otel": no exporters given, so NodeSDK builds them from OTEL_*.
60
+ const sdk = new NodeSDK({ ...config, ...options.sdk });
61
+ sdk.start();
62
+ let stopped;
63
+ return {
64
+ target: resolved.target,
65
+ sdk,
66
+ shutdown: () => (stopped ??= sdk.shutdown().catch(() => { })),
67
+ };
68
+ }
69
+ /**
70
+ * A resource detector for `service.namespace` and
71
+ * `deployment.environment.name`, from the given values or `LNTRN_NAMESPACE`
72
+ * and `LNTRN_ENVIRONMENT`. For a `NodeSDK` you configure yourself:
73
+ *
74
+ * ```ts
75
+ * new NodeSDK({ resourceDetectors: [lanternDetector(), envDetector, processDetector, hostDetector] });
76
+ * ```
77
+ *
78
+ * Put `envDetector` after it, so `OTEL_RESOURCE_ATTRIBUTES` still wins.
79
+ */
80
+ export function lanternDetector(o = {}, env = process.env) {
81
+ return {
82
+ detect: () => {
83
+ const attributes = {};
84
+ const ns = o.namespace || env.LNTRN_NAMESPACE;
85
+ const environment = o.environment || env.LNTRN_ENVIRONMENT;
86
+ if (ns)
87
+ attributes["service.namespace"] = ns;
88
+ if (environment)
89
+ attributes["deployment.environment.name"] = environment;
90
+ return { attributes };
91
+ },
92
+ };
93
+ }
94
+ function resourceAttributes(o, env) {
95
+ const attrs = { ...o.attributes };
96
+ if (o.serviceName)
97
+ attrs["service.name"] = o.serviceName;
98
+ if (o.serviceVersion)
99
+ attrs["service.version"] = o.serviceVersion;
100
+ Object.assign(attrs, lanternDetector(o, env).detect().attributes);
101
+ return attrs;
102
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ /**
10
+ * `node --import @lntrn/otel/register app.js`
11
+ *
12
+ * Starts the SDK from the environment before the app loads:
13
+ * - registers OpenTelemetry's ES module hook, so instrumentations can patch
14
+ * ES modules (pino, express, …) as they are imported;
15
+ * - turns on `@opentelemetry/auto-instrumentations-node` when it is
16
+ * installed (the fs instrumentation stays off: noisy and rarely useful).
17
+ * `LNTRN_INSTRUMENTATIONS=none` skips it;
18
+ * - calls `setup()`, which reads `LNTRN_*` and `OTEL_*`;
19
+ * - flushes before the process exits: when the event loop empties, and on
20
+ * SIGTERM or SIGINT. A signal the app also handles is left to the app;
21
+ * one it doesn't is re-raised after the flush, so the exit code is the
22
+ * usual one.
23
+ */
24
+ import { register } from "node:module";
25
+ import { setup } from "./index.js";
26
+ register("@opentelemetry/instrumentation/hook.mjs", import.meta.url);
27
+ // An optional peer dependency: named through a variable so the build does not
28
+ // need it installed to typecheck.
29
+ const AUTO = "@opentelemetry/auto-instrumentations-node";
30
+ const instrumentations = [];
31
+ if (process.env.LNTRN_INSTRUMENTATIONS?.trim().toLowerCase() !== "none") {
32
+ try {
33
+ const { getNodeAutoInstrumentations } = (await import(__rewriteRelativeImportExtension(AUTO)));
34
+ instrumentations.push(getNodeAutoInstrumentations({ "@opentelemetry/instrumentation-fs": { enabled: false } }));
35
+ }
36
+ catch (err) {
37
+ // Not installed is fine: the app's own spans and logs still go out.
38
+ if (err.code !== "ERR_MODULE_NOT_FOUND")
39
+ throw err;
40
+ }
41
+ }
42
+ const lantern = setup({ instrumentations: instrumentations });
43
+ process.once("beforeExit", () => void lantern.shutdown());
44
+ for (const signal of ["SIGTERM", "SIGINT"]) {
45
+ process.once(signal, () => {
46
+ const appHandles = process.listenerCount(signal) > 0;
47
+ void lantern.shutdown().finally(() => {
48
+ if (!appHandles)
49
+ process.kill(process.pid, signal);
50
+ });
51
+ });
52
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Where to send, and how to authenticate: the part of the add-on with no
3
+ * OpenTelemetry in it, so it can be tested on its own.
4
+ */
5
+ import { TokenCache, type TokenSource } from "@lntrn/device/otel";
6
+ /** Lantern's OTLP edge, for servers: HTTP Basic with an edge credential. */
7
+ export declare const EDGE_URL = "https://otlp.lntrn.dev";
8
+ /** Lantern's client ingest, for devices: a device token as the bearer. */
9
+ export declare const INGEST_URL = "https://in.lntrn.dev";
10
+ /**
11
+ * - `edge`: the organisation's OTLP edge, with an edge credential.
12
+ * - `device`: the client ingest, with a device token.
13
+ * - `gateway`: an in-cluster collector at `endpoint`, with no credential.
14
+ * - `otel`: whatever the standard `OTEL_EXPORTER_OTLP_*` variables say.
15
+ * - `none`: nothing is sent; spans still get ids.
16
+ */
17
+ export type Target = "edge" | "device" | "gateway" | "otel" | "none";
18
+ export interface TargetOptions {
19
+ /** Default: `LNTRN_TARGET`, then the first configured target in the order above. */
20
+ target?: Target;
21
+ /** Replaces the target's base URL (`LNTRN_ENDPOINT`). `/v1/<signal>` is appended. */
22
+ endpoint?: string;
23
+ /** An edge credential (`LNTRN_EDGE_USER`, `LNTRN_EDGE_PASSWORD`). */
24
+ edgeUser?: string;
25
+ edgePassword?: string;
26
+ /**
27
+ * Device tokens for the `device` target: a token, a function returning
28
+ * one (your backend, or `mintToken` from `@lntrn/device/server`), or a
29
+ * `TokenCache`. Default `LNTRN_DEVICE_TOKEN`.
30
+ */
31
+ token?: TokenSource | TokenCache;
32
+ /** Told when no device token can be had; the export then goes without one. */
33
+ onTokenError?: (err: Error) => void;
34
+ }
35
+ /** A resolved target: the base URL and the headers factory for its exporters. */
36
+ export interface ResolvedTarget {
37
+ target: Target;
38
+ /** Undefined for `otel` and `none`, which build no exporters of their own. */
39
+ url?: string;
40
+ headers?: Record<string, string> | (() => Promise<Record<string, string>>);
41
+ }
42
+ type Env = Record<string, string | undefined>;
43
+ export declare function resolveTarget(o: TargetOptions, env?: Env): ResolvedTarget;
44
+ export {};
package/dist/target.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Where to send, and how to authenticate: the part of the add-on with no
3
+ * OpenTelemetry in it, so it can be tested on its own.
4
+ */
5
+ import { TokenCache, deviceTokenHeaders } from "@lntrn/device/otel";
6
+ /** Lantern's OTLP edge, for servers: HTTP Basic with an edge credential. */
7
+ export const EDGE_URL = "https://otlp.lntrn.dev";
8
+ /** Lantern's client ingest, for devices: a device token as the bearer. */
9
+ export const INGEST_URL = "https://in.lntrn.dev";
10
+ const TARGETS = new Set(["edge", "device", "gateway", "otel", "none"]);
11
+ export function resolveTarget(o, env = process.env) {
12
+ let target = o.target ?? (env.LNTRN_TARGET || undefined)?.toLowerCase();
13
+ if (env.OTEL_SDK_DISABLED?.trim().toLowerCase() === "true")
14
+ target = "none";
15
+ const base = o.endpoint || env.LNTRN_ENDPOINT || undefined;
16
+ const user = o.edgeUser || env.LNTRN_EDGE_USER || undefined;
17
+ const pass = o.edgePassword || env.LNTRN_EDGE_PASSWORD || undefined;
18
+ const token = o.token ?? (env.LNTRN_DEVICE_TOKEN || undefined);
19
+ if (target === undefined) {
20
+ if (user || pass)
21
+ target = "edge";
22
+ else if (token)
23
+ target = "device";
24
+ else if (base)
25
+ target = "gateway";
26
+ else if (otelEndpointSet(env))
27
+ target = "otel";
28
+ else
29
+ target = "none";
30
+ }
31
+ if (!TARGETS.has(target)) {
32
+ throw new Error(`lntrn: unknown target "${target}" (want edge, device, gateway, otel or none)`);
33
+ }
34
+ const trim = (u) => u.replace(/\/+$/, "");
35
+ switch (target) {
36
+ case "edge":
37
+ if (!user || !pass) {
38
+ throw new Error("lntrn: the edge target needs both an edge user and password (LNTRN_EDGE_USER, LNTRN_EDGE_PASSWORD)");
39
+ }
40
+ return {
41
+ target,
42
+ url: trim(base ?? EDGE_URL),
43
+ headers: { Authorization: `Basic ${Buffer.from(`${user}:${pass}`).toString("base64")}` },
44
+ };
45
+ case "device":
46
+ if (!token)
47
+ throw new Error("lntrn: the device target needs a token (options.token or LNTRN_DEVICE_TOKEN)");
48
+ return {
49
+ target,
50
+ url: trim(base ?? INGEST_URL),
51
+ headers: deviceTokenHeaders(token, { onError: o.onTokenError }),
52
+ };
53
+ case "gateway":
54
+ if (!base)
55
+ throw new Error("lntrn: the gateway target needs an endpoint (options.endpoint or LNTRN_ENDPOINT)");
56
+ return { target, url: trim(base) };
57
+ default:
58
+ return { target };
59
+ }
60
+ }
61
+ function otelEndpointSet(env) {
62
+ return [
63
+ "OTEL_EXPORTER_OTLP_ENDPOINT",
64
+ "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
65
+ "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
66
+ "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
67
+ ].some((k) => !!env[k]);
68
+ }
package/package.json CHANGED
@@ -1,6 +1,66 @@
1
1
  {
2
2
  "name": "@lntrn/otel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Lantern's add-on for the OpenTelemetry Node SDK: node --import @lntrn/otel/register sends traces, metrics and logs to Lantern's edge or client ingest through the stock NodeSDK and OTLP exporters.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=22.15"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public",
12
+ "registry": "https://registry.npmjs.org"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://git.prod.wtf/xeonr/lantern/otel.git",
17
+ "directory": "node"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "./register": {
30
+ "types": "./dist/register.d.ts",
31
+ "default": "./dist/register.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "dependencies": {
36
+ "@lntrn/device": "^1.3.0",
37
+ "@opentelemetry/exporter-logs-otlp-proto": "^0.300.0",
38
+ "@opentelemetry/exporter-metrics-otlp-proto": "^0.300.0",
39
+ "@opentelemetry/exporter-trace-otlp-proto": "^0.300.0",
40
+ "@opentelemetry/instrumentation": "^0.300.0",
41
+ "@opentelemetry/resources": "^3.0.0",
42
+ "@opentelemetry/sdk-logs": "^3.0.0",
43
+ "@opentelemetry/sdk-metrics": "^3.0.0",
44
+ "@opentelemetry/sdk-node": "^0.300.0",
45
+ "@opentelemetry/sdk-trace": "^3.0.0"
46
+ },
47
+ "peerDependencies": {
48
+ "@opentelemetry/api": ">=1.10.0 <1.11.0",
49
+ "@opentelemetry/auto-instrumentations-node": ">=0.82.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "@opentelemetry/auto-instrumentations-node": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "devDependencies": {
57
+ "@opentelemetry/api": "1.10.0",
58
+ "@types/node": "^22.15.0",
59
+ "typescript": "^5.9.0"
60
+ },
61
+ "scripts": {
62
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/check-dist.mjs",
63
+ "typecheck": "tsc -p tsconfig.json --noEmit",
64
+ "test": "node --experimental-transform-types --no-warnings --test 'src/**/*.test.ts'"
65
+ }
6
66
  }