@ripplo/instrument 0.7.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/LICENSE.md +1 -0
- package/README.md +30 -0
- package/dist/index.js +145 -0
- package/dist/register.d.ts +3 -0
- package/dist/register.js +145 -0
- package/package.json +41 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
© Ripplo LLC. All rights reserved. Use is subject to Ripplo's [Terms of Service](https://ripplo.ai/terms).
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @ripplo/instrument
|
|
2
|
+
|
|
3
|
+
Server-side OpenTelemetry preload for [Ripplo](https://ripplo.ai). Load it in your app server during development and every test run's `behavior.jsonl` includes your backend spans — HTTP handlers, outbound fetches, queue work — correlated to the browser actions that triggered them.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Preload it when starting your dev server:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
node --import @ripplo/instrument server.js
|
|
11
|
+
tsx watch --import @ripplo/instrument src/index.ts
|
|
12
|
+
NODE_OPTIONS="--import @ripplo/instrument" next dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Frameworks with a register hook (Next.js `instrumentation.ts`) can call it directly:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { register } from "@ripplo/instrument/register";
|
|
19
|
+
register();
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
The preload starts a NodeSDK with auto-instrumentation and exports spans over OTLP to the Ripplo daemon's local receiver. The receiver's port is discovered through `.ripplo/.local/otlp-port`, written by `ripplo daemon` for the lifetime of your dev session. When the daemon isn't running, spans are dropped and your server runs as if the preload weren't there — safe to leave in your dev script permanently.
|
|
25
|
+
|
|
26
|
+
Installed by `npx ripplo init`. See the [`ripplo` CLI](https://www.npmjs.com/package/ripplo) for setup.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
© Ripplo LLC. All rights reserved. Use is subject to Ripplo's [Terms of Service](https://ripplo.ai/terms).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/register.ts
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import {
|
|
5
|
+
CompositePropagator,
|
|
6
|
+
ExportResultCode,
|
|
7
|
+
W3CBaggagePropagator,
|
|
8
|
+
W3CTraceContextPropagator
|
|
9
|
+
} from "@opentelemetry/core";
|
|
10
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
11
|
+
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
12
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
13
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
|
|
14
|
+
import { propagation } from "@opentelemetry/api";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
var REQUIRE_PARENT = { requireParentSpan: true };
|
|
17
|
+
var BAGGAGE_ATTRIBUTE_KEYS = ["ripplo.run"];
|
|
18
|
+
var ENDPOINT_CACHE_TTL_MS = 1e3;
|
|
19
|
+
var started = false;
|
|
20
|
+
function register() {
|
|
21
|
+
if (started) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
started = true;
|
|
25
|
+
const sdk = new NodeSDK({
|
|
26
|
+
instrumentations: [
|
|
27
|
+
getNodeAutoInstrumentations({
|
|
28
|
+
"@opentelemetry/instrumentation-dns": { enabled: false },
|
|
29
|
+
"@opentelemetry/instrumentation-fs": { enabled: false },
|
|
30
|
+
"@opentelemetry/instrumentation-graphql": { enabled: false },
|
|
31
|
+
"@opentelemetry/instrumentation-ioredis": REQUIRE_PARENT,
|
|
32
|
+
"@opentelemetry/instrumentation-mongodb": REQUIRE_PARENT,
|
|
33
|
+
"@opentelemetry/instrumentation-net": { enabled: false },
|
|
34
|
+
"@opentelemetry/instrumentation-pg": { enabled: false },
|
|
35
|
+
"@opentelemetry/instrumentation-redis": REQUIRE_PARENT
|
|
36
|
+
})
|
|
37
|
+
],
|
|
38
|
+
spanLimits: { attributeValueLengthLimit: 300 },
|
|
39
|
+
spanProcessors: [
|
|
40
|
+
new BaggageAttributeSpanProcessor(new BatchSpanProcessor(new LazyOtlpExporter()))
|
|
41
|
+
],
|
|
42
|
+
textMapPropagator: new CompositePropagator({
|
|
43
|
+
propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()]
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
sdk.start();
|
|
47
|
+
}
|
|
48
|
+
var portSchema = z.coerce.number().int().positive();
|
|
49
|
+
var LazyOtlpExporter = class {
|
|
50
|
+
cached;
|
|
51
|
+
delegate;
|
|
52
|
+
portFile = resolvePortFile();
|
|
53
|
+
export(spans, resultCallback) {
|
|
54
|
+
const url = this.resolveUrl();
|
|
55
|
+
if (url == null) {
|
|
56
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.exporterFor(url).export(spans, resultCallback);
|
|
60
|
+
}
|
|
61
|
+
forceFlush() {
|
|
62
|
+
return this.delegate?.exporter.forceFlush() ?? Promise.resolve();
|
|
63
|
+
}
|
|
64
|
+
shutdown() {
|
|
65
|
+
return this.delegate?.exporter.shutdown() ?? Promise.resolve();
|
|
66
|
+
}
|
|
67
|
+
resolveUrl() {
|
|
68
|
+
const override = process.env["RIPPLO_OTLP_ENDPOINT"];
|
|
69
|
+
if (override != null) {
|
|
70
|
+
return override;
|
|
71
|
+
}
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
if (this.cached != null && now - this.cached.checkedAt < ENDPOINT_CACHE_TTL_MS) {
|
|
74
|
+
return this.cached.url;
|
|
75
|
+
}
|
|
76
|
+
this.cached = { checkedAt: now, url: readEndpoint(this.portFile) };
|
|
77
|
+
return this.cached.url;
|
|
78
|
+
}
|
|
79
|
+
exporterFor(url) {
|
|
80
|
+
if (this.delegate?.url !== url) {
|
|
81
|
+
void this.delegate?.exporter.shutdown();
|
|
82
|
+
this.delegate = { exporter: new OTLPTraceExporter({ url }), url };
|
|
83
|
+
}
|
|
84
|
+
return this.delegate.exporter;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function readEndpoint(portFile) {
|
|
88
|
+
if (portFile == null) {
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
const raw = readPortFile(portFile);
|
|
92
|
+
if (raw == null) {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
const parsed = portSchema.safeParse(raw.trim());
|
|
96
|
+
return parsed.success ? `http://localhost:${String(parsed.data)}/v1/traces` : void 0;
|
|
97
|
+
}
|
|
98
|
+
function readPortFile(portFile) {
|
|
99
|
+
try {
|
|
100
|
+
return readFileSync(portFile, { encoding: "utf8" });
|
|
101
|
+
} catch {
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function resolvePortFile() {
|
|
106
|
+
const root = findRipploRoot(process.cwd());
|
|
107
|
+
return root == null ? void 0 : path.join(root, ".ripplo", ".local", "otlp-port");
|
|
108
|
+
}
|
|
109
|
+
function findRipploRoot(dir) {
|
|
110
|
+
if (existsSync(path.join(dir, ".ripplo", "project.json"))) {
|
|
111
|
+
return dir;
|
|
112
|
+
}
|
|
113
|
+
const parent = path.dirname(dir);
|
|
114
|
+
return parent === dir ? void 0 : findRipploRoot(parent);
|
|
115
|
+
}
|
|
116
|
+
var BaggageAttributeSpanProcessor = class {
|
|
117
|
+
inner;
|
|
118
|
+
constructor(inner) {
|
|
119
|
+
this.inner = inner;
|
|
120
|
+
}
|
|
121
|
+
onStart(span, parentContext) {
|
|
122
|
+
const baggage = propagation.getBaggage(parentContext);
|
|
123
|
+
if (baggage != null) {
|
|
124
|
+
BAGGAGE_ATTRIBUTE_KEYS.forEach((key) => {
|
|
125
|
+
const entry = baggage.getEntry(key);
|
|
126
|
+
if (entry != null) {
|
|
127
|
+
span.setAttribute(key, entry.value);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
this.inner.onStart(span, parentContext);
|
|
132
|
+
}
|
|
133
|
+
onEnd(span) {
|
|
134
|
+
this.inner.onEnd(span);
|
|
135
|
+
}
|
|
136
|
+
forceFlush() {
|
|
137
|
+
return this.inner.forceFlush();
|
|
138
|
+
}
|
|
139
|
+
shutdown() {
|
|
140
|
+
return this.inner.shutdown();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/index.ts
|
|
145
|
+
register();
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/register.ts
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import {
|
|
5
|
+
CompositePropagator,
|
|
6
|
+
ExportResultCode,
|
|
7
|
+
W3CBaggagePropagator,
|
|
8
|
+
W3CTraceContextPropagator
|
|
9
|
+
} from "@opentelemetry/core";
|
|
10
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
11
|
+
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
12
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
13
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
|
|
14
|
+
import { propagation } from "@opentelemetry/api";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
var REQUIRE_PARENT = { requireParentSpan: true };
|
|
17
|
+
var BAGGAGE_ATTRIBUTE_KEYS = ["ripplo.run"];
|
|
18
|
+
var ENDPOINT_CACHE_TTL_MS = 1e3;
|
|
19
|
+
var started = false;
|
|
20
|
+
function register() {
|
|
21
|
+
if (started) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
started = true;
|
|
25
|
+
const sdk = new NodeSDK({
|
|
26
|
+
instrumentations: [
|
|
27
|
+
getNodeAutoInstrumentations({
|
|
28
|
+
"@opentelemetry/instrumentation-dns": { enabled: false },
|
|
29
|
+
"@opentelemetry/instrumentation-fs": { enabled: false },
|
|
30
|
+
"@opentelemetry/instrumentation-graphql": { enabled: false },
|
|
31
|
+
"@opentelemetry/instrumentation-ioredis": REQUIRE_PARENT,
|
|
32
|
+
"@opentelemetry/instrumentation-mongodb": REQUIRE_PARENT,
|
|
33
|
+
"@opentelemetry/instrumentation-net": { enabled: false },
|
|
34
|
+
"@opentelemetry/instrumentation-pg": { enabled: false },
|
|
35
|
+
"@opentelemetry/instrumentation-redis": REQUIRE_PARENT
|
|
36
|
+
})
|
|
37
|
+
],
|
|
38
|
+
spanLimits: { attributeValueLengthLimit: 300 },
|
|
39
|
+
spanProcessors: [
|
|
40
|
+
new BaggageAttributeSpanProcessor(new BatchSpanProcessor(new LazyOtlpExporter()))
|
|
41
|
+
],
|
|
42
|
+
textMapPropagator: new CompositePropagator({
|
|
43
|
+
propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()]
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
sdk.start();
|
|
47
|
+
}
|
|
48
|
+
var portSchema = z.coerce.number().int().positive();
|
|
49
|
+
var LazyOtlpExporter = class {
|
|
50
|
+
cached;
|
|
51
|
+
delegate;
|
|
52
|
+
portFile = resolvePortFile();
|
|
53
|
+
export(spans, resultCallback) {
|
|
54
|
+
const url = this.resolveUrl();
|
|
55
|
+
if (url == null) {
|
|
56
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this.exporterFor(url).export(spans, resultCallback);
|
|
60
|
+
}
|
|
61
|
+
forceFlush() {
|
|
62
|
+
return this.delegate?.exporter.forceFlush() ?? Promise.resolve();
|
|
63
|
+
}
|
|
64
|
+
shutdown() {
|
|
65
|
+
return this.delegate?.exporter.shutdown() ?? Promise.resolve();
|
|
66
|
+
}
|
|
67
|
+
resolveUrl() {
|
|
68
|
+
const override = process.env["RIPPLO_OTLP_ENDPOINT"];
|
|
69
|
+
if (override != null) {
|
|
70
|
+
return override;
|
|
71
|
+
}
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
if (this.cached != null && now - this.cached.checkedAt < ENDPOINT_CACHE_TTL_MS) {
|
|
74
|
+
return this.cached.url;
|
|
75
|
+
}
|
|
76
|
+
this.cached = { checkedAt: now, url: readEndpoint(this.portFile) };
|
|
77
|
+
return this.cached.url;
|
|
78
|
+
}
|
|
79
|
+
exporterFor(url) {
|
|
80
|
+
if (this.delegate?.url !== url) {
|
|
81
|
+
void this.delegate?.exporter.shutdown();
|
|
82
|
+
this.delegate = { exporter: new OTLPTraceExporter({ url }), url };
|
|
83
|
+
}
|
|
84
|
+
return this.delegate.exporter;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function readEndpoint(portFile) {
|
|
88
|
+
if (portFile == null) {
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
const raw = readPortFile(portFile);
|
|
92
|
+
if (raw == null) {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
const parsed = portSchema.safeParse(raw.trim());
|
|
96
|
+
return parsed.success ? `http://localhost:${String(parsed.data)}/v1/traces` : void 0;
|
|
97
|
+
}
|
|
98
|
+
function readPortFile(portFile) {
|
|
99
|
+
try {
|
|
100
|
+
return readFileSync(portFile, { encoding: "utf8" });
|
|
101
|
+
} catch {
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function resolvePortFile() {
|
|
106
|
+
const root = findRipploRoot(process.cwd());
|
|
107
|
+
return root == null ? void 0 : path.join(root, ".ripplo", ".local", "otlp-port");
|
|
108
|
+
}
|
|
109
|
+
function findRipploRoot(dir) {
|
|
110
|
+
if (existsSync(path.join(dir, ".ripplo", "project.json"))) {
|
|
111
|
+
return dir;
|
|
112
|
+
}
|
|
113
|
+
const parent = path.dirname(dir);
|
|
114
|
+
return parent === dir ? void 0 : findRipploRoot(parent);
|
|
115
|
+
}
|
|
116
|
+
var BaggageAttributeSpanProcessor = class {
|
|
117
|
+
inner;
|
|
118
|
+
constructor(inner) {
|
|
119
|
+
this.inner = inner;
|
|
120
|
+
}
|
|
121
|
+
onStart(span, parentContext) {
|
|
122
|
+
const baggage = propagation.getBaggage(parentContext);
|
|
123
|
+
if (baggage != null) {
|
|
124
|
+
BAGGAGE_ATTRIBUTE_KEYS.forEach((key) => {
|
|
125
|
+
const entry = baggage.getEntry(key);
|
|
126
|
+
if (entry != null) {
|
|
127
|
+
span.setAttribute(key, entry.value);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
this.inner.onStart(span, parentContext);
|
|
132
|
+
}
|
|
133
|
+
onEnd(span) {
|
|
134
|
+
this.inner.onEnd(span);
|
|
135
|
+
}
|
|
136
|
+
forceFlush() {
|
|
137
|
+
return this.inner.forceFlush();
|
|
138
|
+
}
|
|
139
|
+
shutdown() {
|
|
140
|
+
return this.inner.shutdown();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
export {
|
|
144
|
+
register
|
|
145
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ripplo/instrument",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Server-side OpenTelemetry preload for Ripplo — streams backend spans into test runs",
|
|
5
|
+
"homepage": "https://ripplo.ai",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./register": {
|
|
16
|
+
"types": "./dist/register.d.ts",
|
|
17
|
+
"import": "./dist/register.js",
|
|
18
|
+
"default": "./dist/register.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@opentelemetry/api": "^1.9.0",
|
|
23
|
+
"@opentelemetry/auto-instrumentations-node": "^0.56.1",
|
|
24
|
+
"@opentelemetry/core": "^2.7.1",
|
|
25
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
|
|
26
|
+
"@opentelemetry/sdk-node": "^0.200.0",
|
|
27
|
+
"@opentelemetry/sdk-trace-node": "^2.7.1",
|
|
28
|
+
"zod": "4.3.6"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^25.9.1",
|
|
32
|
+
"tsup": "^8.5.1",
|
|
33
|
+
"@ripplo/eslint-config": "0.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"check-types": "tsc --noEmit",
|
|
38
|
+
"lint": "eslint src"
|
|
39
|
+
},
|
|
40
|
+
"main": "./dist/index.js"
|
|
41
|
+
}
|