@ogcio/o11y-sdk-node 0.1.0-beta.3 → 0.1.0-beta.6
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 +21 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3 -0
- package/dist/lib/console.d.ts +3 -0
- package/dist/lib/console.js +12 -0
- package/dist/lib/grpc.d.ts +3 -0
- package/dist/lib/grpc.js +26 -0
- package/dist/lib/http.d.ts +3 -0
- package/dist/lib/http.js +29 -0
- package/dist/lib/index.d.ts +57 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/instrumentation.node.d.ts +3 -0
- package/dist/lib/instrumentation.node.js +73 -0
- package/dist/lib/metrics.d.ts +18 -0
- package/dist/lib/metrics.js +28 -0
- package/dist/lib/options.d.ts +6 -0
- package/dist/lib/options.js +1 -0
- package/dist/lib/url-sampler.d.ts +9 -0
- package/dist/lib/url-sampler.js +28 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.js +5 -0
- package/dist/package.json +49 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +25 -0
- package/lib/index.ts +12 -0
- package/lib/instrumentation.node.ts +12 -1
- package/lib/url-sampler.ts +53 -0
- package/package.json +3 -4
- package/test/integration/README.md +26 -0
- package/test/integration/integration.test.ts +37 -0
- package/test/integration/run.sh +85 -0
- package/test/utils/alloy-log-parser.ts +46 -0
- package/vitest.config.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.0-beta.6](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-node@v0.1.0-beta.5...@ogcio/o11y-sdk-node@v0.1.0-beta.6) (2025-02-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* remove pnpm test on prepublishOnly script ([#68](https://github.com/ogcio/o11y/issues/68)) ([41f6f57](https://github.com/ogcio/o11y/commit/41f6f57fa415c4f7adc29f49f983539274ef7320))
|
|
9
|
+
|
|
10
|
+
## [0.1.0-beta.5](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-node@v0.1.0-beta.4...@ogcio/o11y-sdk-node@v0.1.0-beta.5) (2025-02-06)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add opentelemetry sampler ([#65](https://github.com/ogcio/o11y/issues/65)) ([66793fd](https://github.com/ogcio/o11y/commit/66793fd36bf071e592e3b455f2e33ad9d5b2db37))
|
|
16
|
+
|
|
17
|
+
## [0.1.0-beta.4](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-node@v0.1.0-beta.3...@ogcio/o11y-sdk-node@v0.1.0-beta.4) (2025-01-29)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* prepublishOnly hook ([#60](https://github.com/ogcio/o11y/issues/60)) ([9fbd3ad](https://github.com/ogcio/o11y/commit/9fbd3ad0b45a1604cf2eccc26b2f8855640417a1))
|
|
23
|
+
|
|
3
24
|
## [0.1.0-beta.3](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-node@0.1.0-beta.2...@ogcio/o11y-sdk-node@v0.1.0-beta.3) (2025-01-28)
|
|
4
25
|
|
|
5
26
|
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { NodeSDK } from "@opentelemetry/sdk-node";
|
|
2
|
+
import type { NodeSDKConfig } from "./lib/index.js";
|
|
3
|
+
import buildNodeInstrumentation from "./lib/instrumentation.node.js";
|
|
4
|
+
export type * from "./lib/index.js";
|
|
5
|
+
export type { NodeSDKConfig, NodeSDK };
|
|
6
|
+
export { buildNodeInstrumentation as instrumentNode };
|
|
7
|
+
export * from "./lib/metrics.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { logs, metrics, tracing } from "@opentelemetry/sdk-node";
|
|
2
|
+
export default function buildConsoleExporters(_) {
|
|
3
|
+
return {
|
|
4
|
+
traces: new tracing.ConsoleSpanExporter(),
|
|
5
|
+
metrics: new metrics.PeriodicExportingMetricReader({
|
|
6
|
+
exporter: new metrics.ConsoleMetricExporter(),
|
|
7
|
+
}),
|
|
8
|
+
logs: [
|
|
9
|
+
new logs.SimpleLogRecordProcessor(new logs.ConsoleLogRecordExporter()),
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
}
|
package/dist/lib/grpc.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { LogRecordProcessorMap } from "./utils.js";
|
|
2
|
+
import { metrics } from "@opentelemetry/sdk-node";
|
|
3
|
+
import { CompressionAlgorithm } from "@opentelemetry/otlp-exporter-base";
|
|
4
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
|
|
5
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-grpc";
|
|
6
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
|
|
7
|
+
export default function buildGrpcExporters(config) {
|
|
8
|
+
return {
|
|
9
|
+
traces: new OTLPTraceExporter({
|
|
10
|
+
url: `${config.collectorUrl}`,
|
|
11
|
+
compression: CompressionAlgorithm.GZIP,
|
|
12
|
+
}),
|
|
13
|
+
metrics: new metrics.PeriodicExportingMetricReader({
|
|
14
|
+
exporter: new OTLPMetricExporter({
|
|
15
|
+
url: `${config.collectorUrl}`,
|
|
16
|
+
compression: CompressionAlgorithm.GZIP,
|
|
17
|
+
}),
|
|
18
|
+
}),
|
|
19
|
+
logs: [
|
|
20
|
+
new LogRecordProcessorMap[config.collectorMode ?? "batch"](new OTLPLogExporter({
|
|
21
|
+
url: `${config.collectorUrl}`,
|
|
22
|
+
compression: CompressionAlgorithm.GZIP,
|
|
23
|
+
})),
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
}
|
package/dist/lib/http.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { LogRecordProcessorMap } from "./utils.js";
|
|
2
|
+
import { metrics } from "@opentelemetry/sdk-node";
|
|
3
|
+
import { CompressionAlgorithm } from "@opentelemetry/otlp-exporter-base";
|
|
4
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
5
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
6
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
7
|
+
export default function buildHttpExporters(config) {
|
|
8
|
+
if (config.collectorUrl.endsWith("/")) {
|
|
9
|
+
config.collectorUrl = config.collectorUrl.slice(0, -1);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
traces: new OTLPTraceExporter({
|
|
13
|
+
url: `${config.collectorUrl}/v1/traces`,
|
|
14
|
+
compression: CompressionAlgorithm.GZIP,
|
|
15
|
+
}),
|
|
16
|
+
metrics: new metrics.PeriodicExportingMetricReader({
|
|
17
|
+
exporter: new OTLPMetricExporter({
|
|
18
|
+
url: `${config.collectorUrl}/v1/metrics`,
|
|
19
|
+
compression: CompressionAlgorithm.GZIP,
|
|
20
|
+
}),
|
|
21
|
+
}),
|
|
22
|
+
logs: [
|
|
23
|
+
new LogRecordProcessorMap[config.collectorMode ?? "batch"](new OTLPLogExporter({
|
|
24
|
+
url: `${config.collectorUrl}/v1/logs`,
|
|
25
|
+
compression: CompressionAlgorithm.GZIP,
|
|
26
|
+
})),
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
interface SDKConfig {
|
|
2
|
+
/**
|
|
3
|
+
* The opentelemetry collector entrypoint GRPC url.
|
|
4
|
+
* If the collectoUrl is null or undefined, the instrumentation will not be activated.
|
|
5
|
+
* @example http://alloy:4317
|
|
6
|
+
*/
|
|
7
|
+
collectorUrl: string;
|
|
8
|
+
/**
|
|
9
|
+
* Name of your application used for the collector to group logs
|
|
10
|
+
*/
|
|
11
|
+
serviceName?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Diagnostic log level for the internal runtime instrumentation
|
|
14
|
+
*
|
|
15
|
+
* @type string
|
|
16
|
+
* @default INFO
|
|
17
|
+
*/
|
|
18
|
+
diagLogLevel?: SDKLogLevel;
|
|
19
|
+
/**
|
|
20
|
+
* Collector signals processing mode.
|
|
21
|
+
* signle: makes an http/grpc request for each signal and it is immediately processed inside grafana
|
|
22
|
+
* batch: sends multiple signals within a time window, optimized to reduce http/grpc calls in production
|
|
23
|
+
*
|
|
24
|
+
* @type string
|
|
25
|
+
* @default batch
|
|
26
|
+
*/
|
|
27
|
+
collectorMode?: SDKCollectorMode;
|
|
28
|
+
/**
|
|
29
|
+
* Array of not traced urls.
|
|
30
|
+
*
|
|
31
|
+
* @type {SamplerCondition}
|
|
32
|
+
* @default []
|
|
33
|
+
*/
|
|
34
|
+
ignoreUrls?: SamplerCondition[];
|
|
35
|
+
}
|
|
36
|
+
export interface NodeSDKConfig extends SDKConfig {
|
|
37
|
+
/**
|
|
38
|
+
* Flag to enable or disable the tracing for node:fs module
|
|
39
|
+
*
|
|
40
|
+
* @default false disabling `instrumentation-fs` because it bloating the traces
|
|
41
|
+
*/
|
|
42
|
+
enableFS?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* protocol used to send signals.
|
|
45
|
+
*
|
|
46
|
+
* @default grpc
|
|
47
|
+
*/
|
|
48
|
+
protocol?: SDKProtocol;
|
|
49
|
+
}
|
|
50
|
+
export interface SamplerCondition {
|
|
51
|
+
type: "endsWith" | "includes" | "equals";
|
|
52
|
+
url: string;
|
|
53
|
+
}
|
|
54
|
+
export type SDKCollectorMode = "single" | "batch";
|
|
55
|
+
export type SDKProtocol = "grpc" | "http" | "console";
|
|
56
|
+
export type SDKLogLevel = "NONE" | "ERROR" | "WARN" | "INFO" | "DEBUG" | "VERBOSE" | "ALL";
|
|
57
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { NodeSDK, resources } from "@opentelemetry/sdk-node";
|
|
2
|
+
import buildHttpExporters from "./http.js";
|
|
3
|
+
import buildGrpcExporters from "./grpc.js";
|
|
4
|
+
import buildConsoleExporters from "./console.js";
|
|
5
|
+
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
|
6
|
+
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
7
|
+
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
8
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
9
|
+
import { UrlSampler } from "./url-sampler.js";
|
|
10
|
+
export default function buildNodeInstrumentation(config) {
|
|
11
|
+
if (!config) {
|
|
12
|
+
console.warn("observability config not set. Skipping NodeJS OpenTelemetry instrumentation.");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (!config.collectorUrl) {
|
|
16
|
+
console.warn("collectorUrl not set. Skipping NodeJS OpenTelemetry instrumentation.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (!isUrl(config.collectorUrl)) {
|
|
20
|
+
console.error("collectorUrl does not use a valid format. Skipping NodeJS OpenTelemetry instrumentation.");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
let exporter;
|
|
24
|
+
if (config.protocol === "http") {
|
|
25
|
+
exporter = buildHttpExporters(config);
|
|
26
|
+
}
|
|
27
|
+
else if (config.protocol === "console") {
|
|
28
|
+
exporter = buildConsoleExporters(config);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
exporter = buildGrpcExporters(config);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
diag.setLogger(new DiagConsoleLogger(), config.diagLogLevel
|
|
35
|
+
? DiagLogLevel[config.diagLogLevel]
|
|
36
|
+
: DiagLogLevel.INFO);
|
|
37
|
+
const sdk = new NodeSDK({
|
|
38
|
+
resource: new resources.Resource({
|
|
39
|
+
"o11y.sdk.name": packageJson.name,
|
|
40
|
+
"o11y.sdk.version": packageJson.version,
|
|
41
|
+
}),
|
|
42
|
+
serviceName: config.serviceName,
|
|
43
|
+
traceExporter: exporter.traces,
|
|
44
|
+
metricReader: exporter.metrics,
|
|
45
|
+
logRecordProcessors: exporter.logs,
|
|
46
|
+
sampler: new UrlSampler(config.ignoreUrls),
|
|
47
|
+
textMapPropagator: new W3CTraceContextPropagator(),
|
|
48
|
+
instrumentations: [
|
|
49
|
+
getNodeAutoInstrumentations({
|
|
50
|
+
"@opentelemetry/instrumentation-fs": {
|
|
51
|
+
enabled: config.enableFS ?? false,
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
sdk.start();
|
|
57
|
+
console.log("NodeJS OpenTelemetry instrumentation started successfully.");
|
|
58
|
+
return sdk;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error("Error starting NodeJS OpenTelemetry instrumentation:", error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function isUrl(url) {
|
|
65
|
+
try {
|
|
66
|
+
new URL(url);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.error(err);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Counter, Gauge, Histogram, MetricOptions, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter } from "@opentelemetry/api";
|
|
2
|
+
declare const MetricsMap: {
|
|
3
|
+
gauge: Gauge;
|
|
4
|
+
histogram: Histogram;
|
|
5
|
+
counter: Counter;
|
|
6
|
+
updowncounter: UpDownCounter;
|
|
7
|
+
"async-counter": ObservableCounter;
|
|
8
|
+
"async-updowncounter": ObservableUpDownCounter;
|
|
9
|
+
"async-gauge": ObservableGauge;
|
|
10
|
+
};
|
|
11
|
+
type MetricType = keyof typeof MetricsMap;
|
|
12
|
+
export interface MetricsParams {
|
|
13
|
+
metricName: string;
|
|
14
|
+
attributeName: string;
|
|
15
|
+
options?: MetricOptions;
|
|
16
|
+
}
|
|
17
|
+
export declare function getMetric<T extends MetricType>(type: T, p: MetricsParams): (typeof MetricsMap)[T];
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createNoopMeter, metrics, } from "@opentelemetry/api";
|
|
2
|
+
const MetricsFactoryMap = {
|
|
3
|
+
gauge: (meter) => meter.createGauge,
|
|
4
|
+
histogram: (meter) => meter.createHistogram,
|
|
5
|
+
counter: (meter) => meter.createCounter,
|
|
6
|
+
updowncounter: (meter) => meter.createUpDownCounter,
|
|
7
|
+
"async-counter": (meter) => meter.createObservableCounter,
|
|
8
|
+
"async-updowncounter": (meter) => meter.createObservableUpDownCounter,
|
|
9
|
+
"async-gauge": (meter) => meter.createObservableGauge,
|
|
10
|
+
};
|
|
11
|
+
function getMeter({ metricName, attributeName }) {
|
|
12
|
+
let meter;
|
|
13
|
+
if (!metricName || !attributeName) {
|
|
14
|
+
console.error("Invaid metric configuration!");
|
|
15
|
+
meter = createNoopMeter();
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
meter = metrics.getMeter(`custom_metric.${metricName}`);
|
|
19
|
+
}
|
|
20
|
+
return meter;
|
|
21
|
+
}
|
|
22
|
+
export function getMetric(type, p) {
|
|
23
|
+
const meter = getMeter(p);
|
|
24
|
+
if (!MetricsFactoryMap[type]) {
|
|
25
|
+
throw new Error(`Unsupported metric type: ${type}`);
|
|
26
|
+
}
|
|
27
|
+
return MetricsFactoryMap[type](meter).bind(meter)(p.attributeName, p.options);
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context, SpanKind, Link, Attributes } from "@opentelemetry/api";
|
|
2
|
+
import { Sampler, SamplingResult } from "@opentelemetry/sdk-trace-base";
|
|
3
|
+
import { SamplerCondition } from "./index.js";
|
|
4
|
+
export declare class UrlSampler implements Sampler {
|
|
5
|
+
private _samplerCondition;
|
|
6
|
+
constructor(samplerCondition?: SamplerCondition[]);
|
|
7
|
+
shouldSample(_context: Context, traceId: string, _spanName: string, _spanKind: SpanKind, attributes: Attributes, _links: Link[]): SamplingResult;
|
|
8
|
+
toString(): string;
|
|
9
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isValidTraceId, } from "@opentelemetry/api";
|
|
2
|
+
import { SamplingDecision, } from "@opentelemetry/sdk-trace-base";
|
|
3
|
+
export class UrlSampler {
|
|
4
|
+
_samplerCondition;
|
|
5
|
+
constructor(samplerCondition = []) {
|
|
6
|
+
this._samplerCondition = samplerCondition;
|
|
7
|
+
}
|
|
8
|
+
shouldSample(_context, traceId, _spanName, _spanKind, attributes, _links) {
|
|
9
|
+
const url = attributes["http.target"]?.toString();
|
|
10
|
+
if (url) {
|
|
11
|
+
for (const condition of this._samplerCondition) {
|
|
12
|
+
if ((condition.type === "equals" && url === condition.url) ||
|
|
13
|
+
(condition.type === "endsWith" && url.endsWith(condition.url)) ||
|
|
14
|
+
(condition.type === "includes" && url.includes(condition.url))) {
|
|
15
|
+
return { decision: SamplingDecision.NOT_RECORD };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
decision: isValidTraceId(traceId)
|
|
21
|
+
? SamplingDecision.RECORD_AND_SAMPLED
|
|
22
|
+
: SamplingDecision.NOT_RECORD,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
toString() {
|
|
26
|
+
throw "UrlSampler";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ogcio/o11y-sdk-node",
|
|
3
|
+
"version": "0.1.0-beta.6",
|
|
4
|
+
"description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
9
|
+
"test": "vitest",
|
|
10
|
+
"prepublishOnly": "pnpm i && pnpm build"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/index.js",
|
|
14
|
+
"./*": "./dist/*.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"observability",
|
|
18
|
+
"o11y",
|
|
19
|
+
"opentelemetry",
|
|
20
|
+
"node",
|
|
21
|
+
"nodejs",
|
|
22
|
+
"ogcio"
|
|
23
|
+
],
|
|
24
|
+
"author": "team:ogcio/observability",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@opentelemetry/api": "^1.9.0",
|
|
28
|
+
"@opentelemetry/auto-instrumentations-node": "^0.56.0",
|
|
29
|
+
"@opentelemetry/core": "1.30.1",
|
|
30
|
+
"@opentelemetry/exporter-logs-otlp-grpc": "^0.57.1",
|
|
31
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.57.1",
|
|
32
|
+
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.57.1",
|
|
33
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.57.1",
|
|
34
|
+
"@opentelemetry/exporter-trace-otlp-grpc": "^0.57.1",
|
|
35
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.57.1",
|
|
36
|
+
"@opentelemetry/instrumentation": "^0.57.1",
|
|
37
|
+
"@opentelemetry/otlp-exporter-base": "^0.57.1",
|
|
38
|
+
"@opentelemetry/sdk-metrics": "^1.30.1",
|
|
39
|
+
"@opentelemetry/sdk-node": "^0.57.1",
|
|
40
|
+
"@opentelemetry/sdk-trace-base": "^1.30.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.12.0",
|
|
44
|
+
"@vitest/coverage-v8": "^3.0.4",
|
|
45
|
+
"tsx": "^4.19.2",
|
|
46
|
+
"typescript": "^5.7.3",
|
|
47
|
+
"vitest": "^3.0.4"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
export default defineConfig({
|
|
3
|
+
test: {
|
|
4
|
+
globals: true,
|
|
5
|
+
watch: false,
|
|
6
|
+
include: ["**/test/*.test.ts", "**/test/integration/*.test.ts"],
|
|
7
|
+
exclude: ["**/fixtures/**", "**/dist/**"],
|
|
8
|
+
poolOptions: {
|
|
9
|
+
threads: {
|
|
10
|
+
maxThreads: 8,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
clearMocks: true,
|
|
14
|
+
testTimeout: 30_000,
|
|
15
|
+
coverage: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
provider: "v8",
|
|
18
|
+
reportsDirectory: "coverage",
|
|
19
|
+
reporter: ["lcov", "cobertura"],
|
|
20
|
+
clean: true,
|
|
21
|
+
},
|
|
22
|
+
reporters: ["default", ["junit", { outputFile: "test-report.xml" }]],
|
|
23
|
+
environment: "node",
|
|
24
|
+
},
|
|
25
|
+
});
|
package/lib/index.ts
CHANGED
|
@@ -25,6 +25,13 @@ interface SDKConfig {
|
|
|
25
25
|
* @default batch
|
|
26
26
|
*/
|
|
27
27
|
collectorMode?: SDKCollectorMode;
|
|
28
|
+
/**
|
|
29
|
+
* Array of not traced urls.
|
|
30
|
+
*
|
|
31
|
+
* @type {SamplerCondition}
|
|
32
|
+
* @default []
|
|
33
|
+
*/
|
|
34
|
+
ignoreUrls?: SamplerCondition[];
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
export interface NodeSDKConfig extends SDKConfig {
|
|
@@ -43,6 +50,11 @@ export interface NodeSDKConfig extends SDKConfig {
|
|
|
43
50
|
protocol?: SDKProtocol;
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
export interface SamplerCondition {
|
|
54
|
+
type: "endsWith" | "includes" | "equals";
|
|
55
|
+
url: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
export type SDKCollectorMode = "single" | "batch";
|
|
47
59
|
|
|
48
60
|
export type SDKProtocol = "grpc" | "http" | "console";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { NodeSDK, resources } from "@opentelemetry/sdk-node";
|
|
2
2
|
import type { NodeSDKConfig } from "./index.js";
|
|
3
3
|
import type { Exporters } from "./options.js";
|
|
4
|
-
import isUrl from "is-url";
|
|
5
4
|
import buildHttpExporters from "./http.js";
|
|
6
5
|
import buildGrpcExporters from "./grpc.js";
|
|
7
6
|
import buildConsoleExporters from "./console.js";
|
|
@@ -9,6 +8,7 @@ import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
|
|
9
8
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
10
9
|
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
11
10
|
import packageJson from "../package.json" with { type: "json" };
|
|
11
|
+
import { UrlSampler } from "./url-sampler.js";
|
|
12
12
|
|
|
13
13
|
export default function buildNodeInstrumentation(
|
|
14
14
|
config?: NodeSDKConfig,
|
|
@@ -61,6 +61,7 @@ export default function buildNodeInstrumentation(
|
|
|
61
61
|
traceExporter: exporter.traces,
|
|
62
62
|
metricReader: exporter.metrics,
|
|
63
63
|
logRecordProcessors: exporter.logs,
|
|
64
|
+
sampler: new UrlSampler(config.ignoreUrls),
|
|
64
65
|
textMapPropagator: new W3CTraceContextPropagator(),
|
|
65
66
|
instrumentations: [
|
|
66
67
|
getNodeAutoInstrumentations({
|
|
@@ -81,3 +82,13 @@ export default function buildNodeInstrumentation(
|
|
|
81
82
|
);
|
|
82
83
|
}
|
|
83
84
|
}
|
|
85
|
+
|
|
86
|
+
function isUrl(url: string): boolean {
|
|
87
|
+
try {
|
|
88
|
+
new URL(url);
|
|
89
|
+
return true;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(err);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Context,
|
|
3
|
+
SpanKind,
|
|
4
|
+
Link,
|
|
5
|
+
Attributes,
|
|
6
|
+
isValidTraceId,
|
|
7
|
+
} from "@opentelemetry/api";
|
|
8
|
+
import {
|
|
9
|
+
Sampler,
|
|
10
|
+
SamplingDecision,
|
|
11
|
+
SamplingResult,
|
|
12
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
13
|
+
import { SamplerCondition } from "./index.js";
|
|
14
|
+
|
|
15
|
+
export class UrlSampler implements Sampler {
|
|
16
|
+
private _samplerCondition: SamplerCondition[];
|
|
17
|
+
|
|
18
|
+
constructor(samplerCondition: SamplerCondition[] = []) {
|
|
19
|
+
this._samplerCondition = samplerCondition;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
shouldSample(
|
|
23
|
+
_context: Context,
|
|
24
|
+
traceId: string,
|
|
25
|
+
_spanName: string,
|
|
26
|
+
_spanKind: SpanKind,
|
|
27
|
+
attributes: Attributes,
|
|
28
|
+
_links: Link[],
|
|
29
|
+
): SamplingResult {
|
|
30
|
+
const url: string | undefined = attributes["http.target"]?.toString();
|
|
31
|
+
|
|
32
|
+
if (url) {
|
|
33
|
+
for (const condition of this._samplerCondition) {
|
|
34
|
+
if (
|
|
35
|
+
(condition.type === "equals" && url === condition.url) ||
|
|
36
|
+
(condition.type === "endsWith" && url.endsWith(condition.url)) ||
|
|
37
|
+
(condition.type === "includes" && url.includes(condition.url))
|
|
38
|
+
) {
|
|
39
|
+
return { decision: SamplingDecision.NOT_RECORD };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
decision: isValidTraceId(traceId)
|
|
46
|
+
? SamplingDecision.RECORD_AND_SAMPLED
|
|
47
|
+
: SamplingDecision.NOT_RECORD,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
toString(): string {
|
|
51
|
+
throw "UrlSampler";
|
|
52
|
+
}
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ogcio/o11y-sdk-node",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.6",
|
|
4
4
|
"description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"license": "ISC",
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@opentelemetry/api": "^1.9.0",
|
|
23
|
-
"@opentelemetry/auto-instrumentations-node": "^0.
|
|
23
|
+
"@opentelemetry/auto-instrumentations-node": "^0.56.0",
|
|
24
24
|
"@opentelemetry/core": "1.30.1",
|
|
25
25
|
"@opentelemetry/exporter-logs-otlp-grpc": "^0.57.1",
|
|
26
26
|
"@opentelemetry/exporter-logs-otlp-http": "^0.57.1",
|
|
@@ -32,10 +32,9 @@
|
|
|
32
32
|
"@opentelemetry/otlp-exporter-base": "^0.57.1",
|
|
33
33
|
"@opentelemetry/sdk-metrics": "^1.30.1",
|
|
34
34
|
"@opentelemetry/sdk-node": "^0.57.1",
|
|
35
|
-
"
|
|
35
|
+
"@opentelemetry/sdk-trace-base": "^1.30.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/is-url": "^1.2.32",
|
|
39
38
|
"@types/node": "^22.12.0",
|
|
40
39
|
"@vitest/coverage-v8": "^3.0.4",
|
|
41
40
|
"tsx": "^4.19.2",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Integration Test
|
|
2
|
+
|
|
3
|
+
This folder contains a setup for integration test with o11y node sdk.
|
|
4
|
+
|
|
5
|
+
## Workflow
|
|
6
|
+
|
|
7
|
+
- Docker must be in running state
|
|
8
|
+
- Run the sh script `sh ./packages/sdk-node/test/integration/run.sh 1 .` from project root with following params
|
|
9
|
+
1. pipeline build number, for local development, any number or string is fine
|
|
10
|
+
2. root folder for docker context
|
|
11
|
+
- Change dir to `packages/sdk-node/`
|
|
12
|
+
- Run full test suite with `pnpm test`
|
|
13
|
+
|
|
14
|
+
## Script
|
|
15
|
+
|
|
16
|
+
The `run.sh` script performs the following steps:
|
|
17
|
+
|
|
18
|
+
- build a docker image of a fastify app `/examples/fastify`
|
|
19
|
+
- setup an temporary test docker network
|
|
20
|
+
- run grafana alloy inside a docker container with a test configuration `/alloy/integration-test.alloy`
|
|
21
|
+
- ensure is running otherwise exit process
|
|
22
|
+
- run fastify app in a docker container
|
|
23
|
+
- ensure is running otherwise exit process
|
|
24
|
+
- execute some curl to the fastify microservice
|
|
25
|
+
- persit alloy log to a file and save to following path `/packages/sdk-node/test/integration/`
|
|
26
|
+
- docker turn down process (containers/network/image)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, test, assert } from "vitest";
|
|
2
|
+
import { parseLog } from "../utils/alloy-log-parser";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
describe("instrumentation integration test", () => {
|
|
7
|
+
test("should exclude health url and process only dummy calls", async () => {
|
|
8
|
+
const data = await readFile(join(__dirname, "logs.txt"), "utf-8");
|
|
9
|
+
|
|
10
|
+
let health_traces_counter = 0;
|
|
11
|
+
let dummy_traces_counter = 0;
|
|
12
|
+
|
|
13
|
+
console.log(data.split(/\nts=/).length);
|
|
14
|
+
|
|
15
|
+
for (const line of data.split(/\nts=/)) {
|
|
16
|
+
const parsedLine: Record<string, object | string | number> =
|
|
17
|
+
parseLog(line);
|
|
18
|
+
|
|
19
|
+
if (
|
|
20
|
+
parsedLine["attributes"] &&
|
|
21
|
+
parsedLine["attributes"]["span_kind"] &&
|
|
22
|
+
parsedLine["attributes"]["span_kind"] === "trace"
|
|
23
|
+
) {
|
|
24
|
+
if (parsedLine["attributes"]["http.target"] === "/api/dummy") {
|
|
25
|
+
dummy_traces_counter++;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (parsedLine["attributes"]["http.target"] === "/api/health") {
|
|
29
|
+
health_traces_counter++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
assert.equal(health_traces_counter, 0);
|
|
35
|
+
assert.equal(dummy_traces_counter, 2);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
BUILD_ID=$1
|
|
2
|
+
ROOT_PATH=$2
|
|
3
|
+
|
|
4
|
+
NETWORK_NAME="${BUILD_ID}_testnetwork"
|
|
5
|
+
ALLOY_CONTAINER_NAME="integrationalloy"
|
|
6
|
+
NODE_CONTAINER_NAME="${BUILD_ID}_fastify_app"
|
|
7
|
+
ERROR_CODE=0
|
|
8
|
+
|
|
9
|
+
docker build -t ${NODE_CONTAINER_NAME}:${BUILD_ID} -f $ROOT_PATH/examples/fastify/Dockerfile $ROOT_PATH/
|
|
10
|
+
|
|
11
|
+
docker network create $NETWORK_NAME
|
|
12
|
+
|
|
13
|
+
docker run -d \
|
|
14
|
+
-v "$ROOT_PATH/alloy/integration-test.alloy:/etc/alloy/config.alloy:ro" \
|
|
15
|
+
--network $NETWORK_NAME \
|
|
16
|
+
--name $ALLOY_CONTAINER_NAME \
|
|
17
|
+
-p 4317:4317 \
|
|
18
|
+
-p 4318:4318 \
|
|
19
|
+
grafana/alloy \
|
|
20
|
+
run --server.http.listen-addr=0.0.0.0:12345 --stability.level=experimental /etc/alloy/config.alloy
|
|
21
|
+
|
|
22
|
+
MAX_RETRIES=10
|
|
23
|
+
COUNTER=0
|
|
24
|
+
echo "$ALLOY_CONTAINER_NAME container status"
|
|
25
|
+
until [ "$(docker inspect -f {{.State.Running}} $ALLOY_CONTAINER_NAME)" = true ]; do
|
|
26
|
+
sleep 2
|
|
27
|
+
docker inspect -f {{.State.Running}} $ALLOY_CONTAINER_NAME
|
|
28
|
+
COUNTER=$((COUNTER + 1))
|
|
29
|
+
if [ $COUNTER -ge $MAX_RETRIES ]; then
|
|
30
|
+
echo "Exceeded maximum retries. Exiting."
|
|
31
|
+
ERROR_CODE=1
|
|
32
|
+
break
|
|
33
|
+
fi
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
if [[ $ERROR_CODE -eq 0 ]]; then
|
|
37
|
+
docker run --detach \
|
|
38
|
+
--network $NETWORK_NAME \
|
|
39
|
+
--name $NODE_CONTAINER_NAME \
|
|
40
|
+
--health-cmd="curl -f http://${NODE_CONTAINER_NAME}:9091/api/health > /dev/null || exit 1" \
|
|
41
|
+
--health-start-period=1s \
|
|
42
|
+
--health-retries=10 \
|
|
43
|
+
--health-interval=1s \
|
|
44
|
+
-p 9091:9091 \
|
|
45
|
+
${NODE_CONTAINER_NAME}:${BUILD_ID}
|
|
46
|
+
|
|
47
|
+
COUNTER=0
|
|
48
|
+
echo "$NODE_CONTAINER_NAME container status"
|
|
49
|
+
until [ "$(docker inspect -f {{.State.Health.Status}} $NODE_CONTAINER_NAME)" = "healthy" ]; do
|
|
50
|
+
sleep 1
|
|
51
|
+
docker inspect -f {{.State.Health.Status}} $NODE_CONTAINER_NAME
|
|
52
|
+
COUNTER=$((COUNTER + 1))
|
|
53
|
+
if [ $COUNTER -ge $MAX_RETRIES ]; then
|
|
54
|
+
echo "Exceeded maximum retries. Exiting."
|
|
55
|
+
ERROR_CODE=1
|
|
56
|
+
break
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
if [[ $ERROR_CODE -eq 0 ]]; then
|
|
62
|
+
sleep 2
|
|
63
|
+
curl -X GET -f http://localhost:9091/api/dummy
|
|
64
|
+
sleep 2
|
|
65
|
+
curl -X GET -f http://localhost:9091/api/dummy
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# sleep N seconds to await instrumentation flow send and receiving signals
|
|
69
|
+
sleep 10
|
|
70
|
+
|
|
71
|
+
# Copy logs from container to file
|
|
72
|
+
docker container logs $ALLOY_CONTAINER_NAME >&$ROOT_PATH/packages/sdk-node/test/integration/logs.txt
|
|
73
|
+
echo "log file at $ROOT_PATH/packages/sdk-node/test/integration/logs.txt"
|
|
74
|
+
|
|
75
|
+
docker container stop $ALLOY_CONTAINER_NAME
|
|
76
|
+
docker container stop $NODE_CONTAINER_NAME
|
|
77
|
+
|
|
78
|
+
docker container rm -f $ALLOY_CONTAINER_NAME
|
|
79
|
+
docker container rm -f $NODE_CONTAINER_NAME
|
|
80
|
+
|
|
81
|
+
docker image rm ${NODE_CONTAINER_NAME}:${BUILD_ID}
|
|
82
|
+
|
|
83
|
+
docker network rm $NETWORK_NAME
|
|
84
|
+
|
|
85
|
+
exit $ERROR_CODE
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function parseLog(
|
|
2
|
+
log: string,
|
|
3
|
+
): Record<string, object | string | number> {
|
|
4
|
+
const logArray = log
|
|
5
|
+
.split("\\n")
|
|
6
|
+
.map((line) => line.trim())
|
|
7
|
+
.filter((line) => line);
|
|
8
|
+
|
|
9
|
+
const jsonObject: Record<string, object | string | number> = {};
|
|
10
|
+
let currentSection: Record<string, object | string | number> = jsonObject;
|
|
11
|
+
const sectionStack: Record<string, object | string | number>[] = [];
|
|
12
|
+
|
|
13
|
+
logArray.forEach((line) => {
|
|
14
|
+
line = line.trim();
|
|
15
|
+
|
|
16
|
+
if (line.startsWith("->")) {
|
|
17
|
+
const match = line.match(/->\s+([^:]+):\s+(Str|Int)\((.+)\)/);
|
|
18
|
+
if (match) {
|
|
19
|
+
const [, key, type, value] = match;
|
|
20
|
+
const parsedValue = type === "Int" ? parseInt(value, 10) : value;
|
|
21
|
+
|
|
22
|
+
if (typeof currentSection === "object") {
|
|
23
|
+
currentSection[key] = parsedValue;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} else if (line.endsWith(":")) {
|
|
27
|
+
// new section
|
|
28
|
+
const sectionName = line
|
|
29
|
+
.slice(0, -1)
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(" ", "_");
|
|
33
|
+
jsonObject[sectionName] = {};
|
|
34
|
+
currentSection = jsonObject[sectionName] as Record<
|
|
35
|
+
string,
|
|
36
|
+
object | string | number
|
|
37
|
+
>;
|
|
38
|
+
sectionStack.push(currentSection);
|
|
39
|
+
} else if (line.startsWith('"')) {
|
|
40
|
+
// Additional metadata at the end, store it separately
|
|
41
|
+
jsonObject["metadata"] = line;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return jsonObject;
|
|
46
|
+
}
|
package/vitest.config.ts
CHANGED