@ogcio/o11y-sdk-node 0.4.1 → 0.4.2

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.2](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-node@v0.4.1...@ogcio/o11y-sdk-node@v0.4.2) (2025-09-03)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * withSpan function throws error if sdk is not initialized AB[#30828](https://github.com/ogcio/o11y/issues/30828) ([#200](https://github.com/ogcio/o11y/issues/200)) ([407b616](https://github.com/ogcio/o11y/commit/407b616590024ca4131610c7a69bacd9699f23c5))
9
+
3
10
  ## [0.4.1](https://github.com/ogcio/o11y/compare/@ogcio/o11y-sdk-node@v0.4.0...@ogcio/o11y-sdk-node@v0.4.1) (2025-09-03)
4
11
 
5
12
 
@@ -1,3 +1,3 @@
1
1
  import { NodeSDKConfig } from "./index.js";
2
2
  export declare const setNodeSdkConfig: (config: NodeSDKConfig) => void;
3
- export declare const getNodeSdkConfig: () => NodeSDKConfig;
3
+ export declare const getNodeSdkConfig: () => NodeSDKConfig | undefined;
@@ -3,9 +3,6 @@ export const setNodeSdkConfig = (config) => {
3
3
  nodeSDKConfig = config;
4
4
  };
5
5
  export const getNodeSdkConfig = () => {
6
- if (!nodeSDKConfig) {
7
- throw new Error("Node SDK Config was not initialized.");
8
- }
9
6
  // Ensure getters do not edit config.
10
- return JSON.parse(JSON.stringify(nodeSDKConfig));
7
+ return nodeSDKConfig ? JSON.parse(JSON.stringify(nodeSDKConfig)) : undefined;
11
8
  };
@@ -8,7 +8,7 @@ export declare class PIIExporterDecorator extends OTLPExporterBase<(ReadableSpan
8
8
  private readonly _exporter;
9
9
  private readonly _config;
10
10
  private readonly _redactors;
11
- constructor(exporter: OTLPExporterBase<(ReadableSpan | ReadableLogRecord)[] | ResourceMetrics>, config: NodeSDKConfig);
11
+ constructor(exporter: OTLPExporterBase<(ReadableSpan | ReadableLogRecord)[] | ResourceMetrics>, config: NodeSDKConfig | undefined);
12
12
  forceFlush(): Promise<void>;
13
13
  shutdown(): Promise<void>;
14
14
  export(items: (ReadableSpan | ReadableLogRecord)[] | ResourceMetrics, resultCallback: (result: ExportResult) => void): void;
@@ -9,7 +9,7 @@ export class PIIExporterDecorator extends OTLPExporterBase {
9
9
  super(exporter["_delegate"]);
10
10
  this._exporter = exporter;
11
11
  this._config = config;
12
- this._redactors = this._buildRedactors(config.detection);
12
+ this._redactors = this._buildRedactors(config?.detection);
13
13
  }
14
14
  forceFlush() {
15
15
  return this._exporter.forceFlush();
@@ -45,6 +45,6 @@ export function getActiveSpan() {
45
45
  }
46
46
  export function withSpan({ traceName, spanName, spanOptions = {}, fn, }) {
47
47
  const sdkConfig = getNodeSdkConfig();
48
- const tracer = trace.getTracer(traceName ?? sdkConfig.serviceName ?? "o11y-sdk", sdkConfig.serviceVersion);
48
+ const tracer = trace.getTracer(traceName ?? sdkConfig?.serviceName ?? "o11y-sdk", sdkConfig?.serviceVersion);
49
49
  return tracer.startActiveSpan(spanName, spanOptions, selfContainedSpanHandlerGenerator(fn));
50
50
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-node",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -36,7 +36,7 @@ export default defineConfig({
36
36
  },
37
37
  {
38
38
  test: {
39
- include: ["**/test/integration/*.test.ts"],
39
+ include: ["**/test/integration/**/*.test.ts"],
40
40
  name: "integration",
41
41
  },
42
42
  },
@@ -6,11 +6,7 @@ export const setNodeSdkConfig = (config: NodeSDKConfig) => {
6
6
  nodeSDKConfig = config;
7
7
  };
8
8
 
9
- export const getNodeSdkConfig = (): NodeSDKConfig => {
10
- if (!nodeSDKConfig) {
11
- throw new Error("Node SDK Config was not initialized.");
12
- }
13
-
9
+ export const getNodeSdkConfig = (): NodeSDKConfig | undefined => {
14
10
  // Ensure getters do not edit config.
15
- return JSON.parse(JSON.stringify(nodeSDKConfig));
11
+ return nodeSDKConfig ? JSON.parse(JSON.stringify(nodeSDKConfig)) : undefined;
16
12
  };
@@ -31,12 +31,12 @@ export class PIIExporterDecorator
31
31
  exporter: OTLPExporterBase<
32
32
  (ReadableSpan | ReadableLogRecord)[] | ResourceMetrics
33
33
  >,
34
- config: NodeSDKConfig,
34
+ config: NodeSDKConfig | undefined,
35
35
  ) {
36
36
  super(exporter["_delegate"]);
37
37
  this._exporter = exporter;
38
38
  this._config = config;
39
- this._redactors = this._buildRedactors(config.detection);
39
+ this._redactors = this._buildRedactors(config?.detection);
40
40
  }
41
41
 
42
42
  forceFlush(): Promise<void> {
package/lib/traces.ts CHANGED
@@ -67,8 +67,8 @@ export function withSpan<T>({
67
67
  }: WithSpanParams<T>) {
68
68
  const sdkConfig = getNodeSdkConfig();
69
69
  const tracer = trace.getTracer(
70
- traceName ?? sdkConfig.serviceName ?? "o11y-sdk",
71
- sdkConfig.serviceVersion,
70
+ traceName ?? sdkConfig?.serviceName ?? "o11y-sdk",
71
+ sdkConfig?.serviceVersion,
72
72
  );
73
73
  return tracer.startActiveSpan(
74
74
  spanName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogcio/o11y-sdk-node",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Opentelemetry standard instrumentation SDK for NodeJS based project",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -3,8 +3,8 @@ import { getNodeSdkConfig, setNodeSdkConfig } from "../lib/config-manager";
3
3
  import { NodeSDKConfig } from "../lib";
4
4
 
5
5
  describe("Config Manager", () => {
6
- it("throws if getConfig is called before initialization", () => {
7
- expect(() => getNodeSdkConfig()).toThrow();
6
+ it("does not throw if getConfig is called before initialization", () => {
7
+ expect(() => getNodeSdkConfig()).not.toThrow();
8
8
  });
9
9
 
10
10
  it("sdk defined config is not pollutable", () => {
@@ -1,26 +1,74 @@
1
- # Integration Test
1
+ # Integration Test Runner
2
2
 
3
- This folder contains a setup for integration test with o11y node sdk.
3
+ This script is a **Bash-based test runner** that discovers and executes integration test scripts for the Node SDK (`./packages/sdk-node/test/integration`).
4
+ It ensures tests run inside an isolated Docker network and provides a summary of results.
4
5
 
5
- ## Workflow
6
+ ---
6
7
 
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`
8
+ ## Usage
13
9
 
14
- ## Script
10
+ ```bash
11
+ ./run-tests.sh <build-id> <root-path> <is-ci?>
12
+ ```
13
+
14
+ **Arguments**
15
+
16
+ <build-id>
17
+ A unique identifier for the test run. Used to name the Docker network (<build-id>_testnetwork).
18
+
19
+ <root-path>
20
+ The root path for the project or build environment.
21
+
22
+ [ci-flag] (optional)
23
+ Defaults to false.
24
+ If set to true, the script will remove all Docker images after the tests complete (intended for CI pipelines to save disk space).
25
+
26
+ ## How It Works
27
+
28
+ - Ensures at least two arguments are provided. Exits with usage instructions otherwise.
29
+ - Creates a temporary Docker network named <build-id>\_testnetwork.
30
+ - Finds all integration test scripts under: ./packages/sdk-node/test/integration/test\*/run.sh
31
+ - Runs each test script.
32
+ - Captures exit codes and tracks failures.
33
+ - Prints results per test.
34
+ - Removes the Docker network after all tests finish.
35
+ - If running in CI mode (ci-flag = true), removes all Docker images.
36
+ - Reports whether all tests passed or how many failed.
37
+ Exits with:
38
+ - 0 if all tests succeeded
39
+ - 1 if one or more tests failed
40
+
41
+ **Example**
42
+
43
+ ```
44
+ # Run tests locally
45
+ ./run-tests.sh my-build-id /path/to/project
46
+
47
+ # Run tests in CI mode (cleanup Docker images after)
48
+ ./run-tests.sh my-build-id /path/to/project true
49
+ ```
50
+
51
+ ## Tests
52
+
53
+ ### test_fastify-o11y-pii-enabled
15
54
 
16
55
  The `run.sh` script performs the following steps:
17
56
 
18
57
  - build a docker image of a fastify app `/examples/fastify`
19
- - setup an temporary test docker network
20
58
  - run grafana alloy inside a docker container with a test configuration `/alloy/integration-test.alloy`
21
59
  - ensure is running otherwise exit process
22
60
  - run fastify app in a docker container
23
61
  - ensure is running otherwise exit process
24
62
  - execute some curl to the fastify microservice
25
63
  - persist alloy log to a file and save to following path `/packages/sdk-node/test/integration/`
64
+ - verify pii data are removed and metrics are increased
26
65
  - docker turn down process (containers/network/image)
66
+
67
+ ### test_without-o11y
68
+
69
+ The `run.sh` script performs the following steps:
70
+
71
+ - build a docker image of a fastify app `/examples/fastify`
72
+ - verify service execution with o11y sdk disabled (otel collector empty)
73
+ - execute http requests for dummy route with otel trace customization function calls
74
+ - assert 200 http status without any error
@@ -0,0 +1,214 @@
1
+ #!/bin/bash
2
+
3
+ : '
4
+ Build a new docker image of a given Dockerfile, if already exists skip build.
5
+
6
+ @param container_name docker container name
7
+
8
+ @return int error code
9
+ 0. success
10
+ 1. validation error
11
+ '
12
+ build_image() {
13
+ if [ "$#" -lt 3 ] || [ "$#" -gt 4 ]; then
14
+ echo "Error: build_image expected 3 or 4 parameters, got $#." >&2
15
+ echo "Usage: build_image <image_name> <dockerfile_path> <context_path> [max_age_minutes]" >&2
16
+ return 1
17
+ fi
18
+
19
+
20
+ local image_name="$1"
21
+ local dockerfile_path="$2"
22
+ local context_path="$3"
23
+ local max_age_minutes="${4:-30}" # default threshold = 30 minutes
24
+
25
+ if docker image inspect "$image_name" > /dev/null 2>&1; then
26
+ echo "Image $image_name already exists. Checking age..."
27
+
28
+ # Extract the Created timestamp from docker inspect (ISO 8601 format)
29
+ local created_at
30
+ created_at=$(docker image inspect --format '{{.Created}}' "$image_name")
31
+ if [ -z "$created_at" ]; then
32
+ echo "Warning: Could not determine creation date. Proceeding without rebuild check." >&2
33
+ return 0
34
+ fi
35
+
36
+ # Convert created_at to epoch seconds
37
+ local created_epoch
38
+ created_epoch=$(date -d "$created_at" +%s 2>/dev/null)
39
+
40
+ # Current time in epoch seconds
41
+ local now_epoch
42
+ now_epoch=$(date +%s)
43
+
44
+ # Calculate age in minutes
45
+ local age_minutes=$(( (now_epoch - created_epoch) / 60 ))
46
+
47
+ echo "Image $image_name is $age_minutes minutes old (threshold: $max_age_minutes minutes)."
48
+
49
+ if [ "$age_minutes" -ge "$max_age_minutes" ]; then
50
+ echo "Image is too old. Rebuilding..."
51
+ docker build -t $image_name -f $dockerfile_path $context_path
52
+ return $?
53
+ else
54
+ echo "Image is recent. Skipping rebuild."
55
+ return 0
56
+ fi
57
+ else
58
+ echo "Image $image_name not found. Building..."
59
+ docker build -t "$image_name" -f "$dockerfile_path" "$context_path"
60
+ return $?
61
+ fi
62
+ }
63
+
64
+ : '
65
+ Stop and remove a docker container for the given input container name.
66
+
67
+ @param container_name docker container name
68
+
69
+ @return int error code
70
+ 0. success
71
+ 1. validation error
72
+ '
73
+ container_stop_and_rm() {
74
+ local container_name="$1"
75
+
76
+ if [[ -z "$container_name" ]]; then
77
+ echo "Error: container_name must have a value."
78
+ return 1
79
+ fi
80
+
81
+ docker container stop $container_name
82
+ docker container rm -f $container_name
83
+
84
+ return $?
85
+ }
86
+
87
+ : '
88
+ Run a new alloy container and check if the status is running.
89
+
90
+ @param root_path volume root path for alloy file config
91
+ @param network_name docker network name
92
+ @param container_name docker container name
93
+
94
+ @optional max_retries
95
+ @default 10
96
+
97
+ @return int error code
98
+ 0. success
99
+ 1. validation error
100
+ 2. execution failed
101
+ '
102
+ run_and_check_alloy() {
103
+
104
+ if [ "$#" -lt 3 ]; then
105
+ echo "Error: run_and_check_alloy expected 3 parameters, got $#." >&2
106
+ return 1
107
+ fi
108
+
109
+ local root_path="$1"
110
+ local network_name="$2"
111
+ local container_name="$3"
112
+ local max_retries="${4:-10}"
113
+ local counter_retries=0
114
+
115
+ if [[ -z "$root_path" || -z "$network_name" || -z "$container_name" ]]; then
116
+ echo "Error: root_path, network_name, and container_name must all have values."
117
+ return 1
118
+ fi
119
+
120
+ if ! [[ "$max_retries" =~ ^[0-9]+$ ]]; then
121
+ echo "Error: max_retries parameter must be a number." >&2
122
+ return 1
123
+ fi
124
+
125
+ docker run -d \
126
+ -v "$root_path/alloy/integration-test.alloy:/etc/alloy/config.alloy:ro" \
127
+ --network $network_name \
128
+ --name $container_name \
129
+ -p 4317:4317 \
130
+ -p 4318:4318 \
131
+ grafana/alloy:v1.9.2 \
132
+ run --server.http.listen-addr=0.0.0.0:12345 --stability.level=experimental /etc/alloy/config.alloy
133
+
134
+ echo "$container_name container status"
135
+ until [ "$(docker inspect -f {{.State.Running}} $container_name)" = true ]; do
136
+ sleep 2
137
+ docker inspect -f {{.State.Running}} $container_name
138
+ counter_retries=$((counter_retries + 1))
139
+ if [ $counter_retries -ge $max_retries ]; then
140
+ echo "Exceeded maximum retries. Exiting."
141
+ return 2
142
+ fi
143
+ done
144
+
145
+ return 0
146
+ }
147
+
148
+ : '
149
+ Run a new fastify microservice container and check if the status is running.
150
+
151
+ @param network_name docker network name
152
+ @param collector_url otel collector grpc endpoint
153
+ @param container_name docker container name
154
+ @param build_id current pipeline build id
155
+
156
+ @optional max_retries
157
+ @default 10
158
+
159
+ @return int error code
160
+ 0. success
161
+ 1. validation error
162
+ 2. execution failed
163
+ '
164
+ run_and_check_fastify() {
165
+
166
+ if [ "$#" -lt 4 ]; then
167
+ echo "Error: run_and_check_alloy expected 3 parameters, got $#." >&2
168
+ return 1
169
+ fi
170
+
171
+ local network_name="$1"
172
+ local collector_url="$2"
173
+ local container_name="$3"
174
+ local build_id="$4"
175
+ local max_retries="${5:-10}"
176
+ local counter_retries=0
177
+
178
+ if [[ -z "$build_id" || -z "$network_name" || -z "$container_name" ]]; then
179
+ echo "Error: build_id, network_name, and container_name must all have values."
180
+ return 1
181
+ fi
182
+
183
+ if ! [[ "$max_retries" =~ ^[0-9]+$ ]]; then
184
+ echo "Error: max_retries parameter must be a number." >&2
185
+ return 1
186
+ fi
187
+
188
+ docker run --detach \
189
+ --network $network_name \
190
+ --name $container_name \
191
+ -e DB_DISABLED="true" \
192
+ -e SERVER_HOST="0.0.0.0" \
193
+ -e OTEL_COLLECTOR_URL=$collector_url \
194
+ --health-cmd="curl -f http://${container_name}:9091/api/health > /dev/null || exit 1" \
195
+ --health-start-period=1s \
196
+ --health-retries=10 \
197
+ --health-interval=1s \
198
+ -p 9091:9091 \
199
+ $container_name:$build_id
200
+
201
+ counter_retries=0
202
+ echo "$container_name container status"
203
+ until [ "$(docker inspect -f {{.State.Health.Status}} $container_name)" = "healthy" ]; do
204
+ sleep 1
205
+ docker inspect -f {{.State.Health.Status}} $container_name
206
+ counter_retries=$((counter_retries + 1))
207
+ if [ $counter_retries -ge $max_retries ]; then
208
+ echo "Exceeded maximum retries. Exiting."
209
+ return 2
210
+ fi
211
+ done
212
+
213
+ return 0
214
+ }
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+ set -o pipefail
5
+
6
+ if [ $# -lt 2 ]; then
7
+ echo "Usage: $0 <build-id> <root-path>"
8
+ exit 1
9
+ fi
10
+
11
+ BUILD_ID=$1
12
+ ROOT_PATH=$2
13
+
14
+ NETWORK_NAME="${BUILD_ID}_testnetwork"
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+
17
+ docker network create $NETWORK_NAME
18
+
19
+ TEST_SCRIPTS=( $(find . -type f -path "./packages/sdk-node/test/integration/test*/run.sh" | sort) )
20
+
21
+ FAILURES=0
22
+
23
+ for test_script in "${TEST_SCRIPTS[@]}"; do
24
+ TEST_DIR=$(dirname "$test_script")
25
+ TEST_NAME=$(basename "$TEST_DIR")
26
+
27
+ echo "Running test: $TEST_NAME ($test_script)"
28
+
29
+ # Run the test
30
+ bash "$test_script" "$BUILD_ID" "$ROOT_PATH" "$NETWORK_NAME"
31
+ EXIT_CODE=$?
32
+
33
+ if [ $EXIT_CODE -eq 0 ]; then
34
+ echo "$test_script completed"
35
+ else
36
+ echo "❌ $test_script failed with exit code $EXIT_CODE"
37
+ FAILURES=$((FAILURES + 1))
38
+ fi
39
+ echo "------------------------------"
40
+ done
41
+
42
+ # Remove network and images from docker host
43
+ bash "$SCRIPT_DIR/teardown.sh" "$BUILD_ID" "$NETWORK_NAME"
44
+
45
+ # Final summary
46
+ if [ $FAILURES -eq 0 ]; then
47
+ echo "All tests ready!"
48
+ exit 0
49
+ else
50
+ echo "$FAILURES test(s) failed."
51
+ exit 1
52
+ fi
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+
3
+ BUILD_ID=$1
4
+ NETWORK_NAME=$2
5
+
6
+ docker network rm $NETWORK_NAME
7
+ docker rmi -f $(docker images --filter "reference=*:$BUILD_ID" -q)
@@ -1,5 +1,5 @@
1
1
  import { describe, test, assert } from "vitest";
2
- import { parseLog } from "../utils/alloy-log-parser";
2
+ import { parseLog } from "../../utils/alloy-log-parser";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { describe, test, assert } from "vitest";
2
- import { parseLog } from "../utils/alloy-log-parser";
2
+ import { parseLog } from "../../utils/alloy-log-parser";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
 
@@ -0,0 +1,42 @@
1
+ #!/bin/bash
2
+ # @docs: packages/sdk-node/test/integration/README.md#test_fastify-o11y-pii-enabled
3
+ BUILD_ID=$1
4
+ ROOT_PATH=$2
5
+ NETWORK_NAME=$3
6
+
7
+ ALLOY_CONTAINER_NAME="integrationalloy"
8
+ NODE_CONTAINER_NAME="${BUILD_ID}_fastify_app"
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ LOG_FILE="$SCRIPT_DIR/logs.txt"
11
+ ERROR_CODE=0
12
+
13
+ source "$SCRIPT_DIR/../docker-utils.sh"
14
+
15
+ build_image "${NODE_CONTAINER_NAME}:${BUILD_ID}" "$ROOT_PATH/examples/fastify/Dockerfile" "$ROOT_PATH/"
16
+
17
+ run_and_check_alloy $ROOT_PATH $NETWORK_NAME $ALLOY_CONTAINER_NAME
18
+ ERROR_CODE=$?
19
+
20
+ if [[ $ERROR_CODE -eq 0 ]]; then
21
+ run_and_check_fastify $NETWORK_NAME "http://integrationalloy:4317" $NODE_CONTAINER_NAME $BUILD_ID
22
+ ERROR_CODE=$?
23
+ fi
24
+
25
+ if [[ $ERROR_CODE -eq 0 ]]; then
26
+ sleep 2
27
+ curl -X GET -f http://localhost:9091/api/dummy -H 'Content-Type: application/json'
28
+ sleep 2
29
+ curl -X GET -f http://localhost:9091/api/dummy -H 'Content-Type: application/json'
30
+ fi
31
+
32
+ # sleep N seconds to await instrumentation flow send and receiving signals
33
+ sleep 1
34
+
35
+ # Copy logs from container to file
36
+ docker container logs $ALLOY_CONTAINER_NAME >&$LOG_FILE
37
+ echo "log file at $LOG_FILE"
38
+
39
+ container_stop_and_rm $ALLOY_CONTAINER_NAME
40
+ container_stop_and_rm $NODE_CONTAINER_NAME
41
+
42
+ exit $ERROR_CODE
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+ # @docs: packages/sdk-node/test/integration/README.md#test_without-o11y
3
+ BUILD_ID=$1
4
+ ROOT_PATH=$2
5
+ NETWORK_NAME=$3
6
+
7
+ NODE_CONTAINER_NAME="${BUILD_ID}_fastify_app"
8
+ ERROR_CODE=0
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ LOG_FILE="$SCRIPT_DIR/logs.txt"
11
+
12
+ source "$SCRIPT_DIR/../docker-utils.sh"
13
+
14
+ build_image "${NODE_CONTAINER_NAME}:${BUILD_ID}" "$ROOT_PATH/examples/fastify/Dockerfile" "$ROOT_PATH/"
15
+
16
+ run_and_check_fastify $NETWORK_NAME "" $NODE_CONTAINER_NAME $BUILD_ID
17
+ ERROR_CODE=$?
18
+
19
+ if [[ $ERROR_CODE -eq 0 ]]; then
20
+ curl -X GET -f http://localhost:9091/api/dummy -H 'Content-Type: application/json'
21
+ sleep 1
22
+ curl -X GET -f http://localhost:9091/api/dummy -H 'Content-Type: application/json'
23
+ fi
24
+
25
+ docker container logs $NODE_CONTAINER_NAME >&$LOG_FILE
26
+ echo "log file at $LOG_FILE"
27
+
28
+ container_stop_and_rm $NODE_CONTAINER_NAME
29
+
30
+ exit $ERROR_CODE
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ describe("Log validation for /api/dummy with instrumentation disabled", async () => {
6
+ const log = await readFile(join(__dirname, "logs.txt"), "utf-8");
7
+
8
+ it("should contain message that collector URL is not set", () => {
9
+ expect(log).toContain(
10
+ "collectorUrl not set. Skipping NodeJS OpenTelemetry instrumentation.",
11
+ );
12
+ });
13
+
14
+ it("should have all /api/dummy responses with status code 200", () => {
15
+ // Extract all request IDs for /api/dummy requests
16
+ const dummyRequestIds = [
17
+ ...log.matchAll(/"reqId":"(.*?)","req":\{[^}]*"url":"\/api\/dummy"/g),
18
+ ].map((match) => match[1]);
19
+
20
+ expect(dummyRequestIds.length).toBeGreaterThan(0);
21
+
22
+ // For each /api/dummy request ID, verify that its response status is 200
23
+ const non200Responses = dummyRequestIds.filter((reqId) => {
24
+ const responseMatch = log.match(
25
+ new RegExp(`"reqId":"${reqId}".*"statusCode":(\\d+)`),
26
+ );
27
+ return responseMatch && Number(responseMatch[1]) !== 200;
28
+ });
29
+
30
+ expect(non200Responses).toEqual([]); // No non-200 responses allowed
31
+ });
32
+ });
@@ -1,8 +1,6 @@
1
- import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
- import * as piiDetection from "../../lib/internals/redaction/pii-detection.js";
3
- import { getActiveSpan } from "../../lib/traces.js";
4
- import { MockSpan } from "../utils/mock-signals.js";
1
+ import { describe, expect, it, vi } from "vitest";
5
2
  import { setNodeSdkConfig } from "../../lib/config-manager.js";
3
+ import { getActiveSpan } from "../../lib/traces.js";
6
4
 
7
5
  describe("getActiveSpan", () => {
8
6
  it("returns undefined if no active span", async () => {
@@ -10,6 +10,22 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
10
10
  import { setNodeSdkConfig } from "../../lib/config-manager.js";
11
11
  import { getActiveSpan, withSpan } from "../../lib/traces.js";
12
12
 
13
+ describe("withSpan with opentelemetry not initialized", () => {
14
+ it("should handle noop span", async ({}) => {
15
+ let capturedSpan: Span;
16
+
17
+ await withSpan({
18
+ spanName: "test-sync-span",
19
+ fn: (span: Span) => {
20
+ capturedSpan = span;
21
+ },
22
+ });
23
+
24
+ expect(capturedSpan).not.toBeNull();
25
+ expect(capturedSpan.constructor.name).toBe("NonRecordingSpan");
26
+ });
27
+ });
28
+
13
29
  describe("withSpan", () => {
14
30
  let memoryExporter: InMemorySpanExporter;
15
31
  let spanProcessor: SpanProcessor;
package/vitest.config.ts CHANGED
@@ -37,7 +37,7 @@ export default defineConfig({
37
37
  },
38
38
  {
39
39
  test: {
40
- include: ["**/test/integration/*.test.ts"],
40
+ include: ["**/test/integration/**/*.test.ts"],
41
41
  name: "integration",
42
42
  },
43
43
  },
@@ -1,88 +0,0 @@
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:v1.9.2 \
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
- -e DB_DISABLED="true" \
41
- -e SERVER_HOST="0.0.0.0" \
42
- -e OTEL_COLLECTOR_URL="http://integrationalloy:4317" \
43
- --health-cmd="curl -f http://${NODE_CONTAINER_NAME}:9091/api/health > /dev/null || exit 1" \
44
- --health-start-period=1s \
45
- --health-retries=10 \
46
- --health-interval=1s \
47
- -p 9091:9091 \
48
- ${NODE_CONTAINER_NAME}:${BUILD_ID}
49
-
50
- COUNTER=0
51
- echo "$NODE_CONTAINER_NAME container status"
52
- until [ "$(docker inspect -f {{.State.Health.Status}} $NODE_CONTAINER_NAME)" = "healthy" ]; do
53
- sleep 1
54
- docker inspect -f {{.State.Health.Status}} $NODE_CONTAINER_NAME
55
- COUNTER=$((COUNTER + 1))
56
- if [ $COUNTER -ge $MAX_RETRIES ]; then
57
- echo "Exceeded maximum retries. Exiting."
58
- ERROR_CODE=1
59
- break
60
- fi
61
- done
62
- fi
63
-
64
- if [[ $ERROR_CODE -eq 0 ]]; then
65
- sleep 2
66
- curl -X GET -f http://localhost:9091/api/dummy -H 'Content-Type: application/json'
67
- sleep 2
68
- curl -X GET -f http://localhost:9091/api/dummy -H 'Content-Type: application/json'
69
- fi
70
-
71
- # sleep N seconds to await instrumentation flow send and receiving signals
72
- sleep 1
73
-
74
- # Copy logs from container to file
75
- docker container logs $ALLOY_CONTAINER_NAME >&$ROOT_PATH/packages/sdk-node/test/integration/logs.txt
76
- echo "log file at $ROOT_PATH/packages/sdk-node/test/integration/logs.txt"
77
-
78
- docker container stop $ALLOY_CONTAINER_NAME
79
- docker container stop $NODE_CONTAINER_NAME
80
-
81
- docker container rm -f $ALLOY_CONTAINER_NAME
82
- docker container rm -f $NODE_CONTAINER_NAME
83
-
84
- docker image rm ${NODE_CONTAINER_NAME}:${BUILD_ID}
85
-
86
- docker network rm $NETWORK_NAME
87
-
88
- exit $ERROR_CODE