@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 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
 
@@ -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,3 @@
1
+ import buildNodeInstrumentation from "./lib/instrumentation.node.js";
2
+ export { buildNodeInstrumentation as instrumentNode };
3
+ export * from "./lib/metrics.js";
@@ -0,0 +1,3 @@
1
+ import type { NodeSDKConfig } from "./index.js";
2
+ import type { Exporters } from "./options.js";
3
+ export default function buildConsoleExporters(_: NodeSDKConfig): Exporters;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import type { NodeSDKConfig } from "./index.js";
2
+ import type { Exporters } from "./options.js";
3
+ export default function buildGrpcExporters(config: NodeSDKConfig): Exporters;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import type { NodeSDKConfig } from "./index.js";
2
+ import type { Exporters } from "./options.js";
3
+ export default function buildHttpExporters(config: NodeSDKConfig): Exporters;
@@ -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,3 @@
1
+ import { NodeSDK } from "@opentelemetry/sdk-node";
2
+ import type { NodeSDKConfig } from "./index.js";
3
+ export default function buildNodeInstrumentation(config?: NodeSDKConfig): NodeSDK | undefined;
@@ -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,6 @@
1
+ import type { logs, tracing, metrics } from "@opentelemetry/sdk-node";
2
+ export type Exporters = {
3
+ traces: tracing.SpanExporter;
4
+ metrics: metrics.MetricReader;
5
+ logs: logs.LogRecordProcessor[];
6
+ };
@@ -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,3 @@
1
+ import { SDKCollectorMode } from "./index.js";
2
+ import { logs } from "@opentelemetry/sdk-node";
3
+ export declare const LogRecordProcessorMap: Record<SDKCollectorMode, typeof logs.SimpleLogRecordProcessor | typeof logs.BatchLogRecordProcessor>;
@@ -0,0 +1,5 @@
1
+ import { logs } from "@opentelemetry/sdk-node";
2
+ export const LogRecordProcessorMap = {
3
+ single: logs.SimpleLogRecordProcessor,
4
+ batch: logs.BatchLogRecordProcessor,
5
+ };
@@ -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,2 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
@@ -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",
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.55.3",
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
- "is-url": "^1.2.4"
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
@@ -4,7 +4,7 @@ export default defineConfig({
4
4
  test: {
5
5
  globals: true,
6
6
  watch: false,
7
- include: ["**/test/*.test.ts"],
7
+ include: ["**/test/*.test.ts", "**/test/integration/*.test.ts"],
8
8
  exclude: ["**/fixtures/**", "**/dist/**"],
9
9
  poolOptions: {
10
10
  threads: {