@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 +18 -0
- package/README.md +121 -2
- package/dist/index.d.ts +76 -0
- package/dist/index.js +102 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +52 -0
- package/dist/target.d.ts +44 -0
- package/dist/target.js +68 -0
- package/package.json +63 -3
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
|
-
#
|
|
1
|
+
# @lntrn/otel
|
|
2
2
|
|
|
3
|
-
|
|
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`.
|
package/dist/index.d.ts
ADDED
|
@@ -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 {};
|
package/dist/register.js
ADDED
|
@@ -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
|
+
}
|
package/dist/target.d.ts
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
}
|