Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: minium version check #5

Merged
merged 1 commit into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/verify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,33 @@ jobs:
- name: test
run: |
npm test

verify-minimum-version-check:

runs-on: ubuntu-latest

strategy:
matrix:
node-version: [ '16.x' ]

name: Verify Minimum Version Check (Node.js ${{ matrix.node-version }})

steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

- name: versions
run: |
node --version
npm --version

- run: npm ci

- name: integration test
run: |
npm run test:integration
26 changes: 4 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"nyc": "^15.1.0",
"prettier": "^3.2.5",
"protobufjs": "^7.2.6",
"semver": "^7.6.2",
"sinon": "^17.0.1",
"ts-node": "^10.9.2",
"ts-proto": "^1.172.0",
Expand Down
100 changes: 38 additions & 62 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,47 @@
// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0

import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
import { Detector, DetectorSync, envDetector, hostDetector, processDetector, Resource } from '@opentelemetry/resources';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { NodeSDK, NodeSDKConfiguration } from '@opentelemetry/sdk-node';

import { version } from '../package.json';
import PodUidDetector from './detectors/node/opentelemetry-resource-detector-kubernetes-pod';
import ServiceNameFallbackDetector from './detectors/node/opentelemetry-resource-detector-service-name-fallback';
import { getResourceDetectorsFromEnv } from './util/sdkUtil';

if (process.env.DASH0_DEBUG) {
console.log('Dash0 OpenTelemetry distribution for Node.js: Starting NodeSDK.');
}

let baseUrl = 'http://dash0-opentelemetry-collector-daemonset.default.svc.cluster.local:4318';
if (process.env.DASH0_OTEL_COLLECTOR_BASE_URL) {
baseUrl = process.env.DASH0_OTEL_COLLECTOR_BASE_URL;
}

const instrumentationConfig: any = {};
if (
!process.env.DASH0_ENABLE_FS_INSTRUMENTATION ||
process.env.DASH0_ENABLE_FS_INSTRUMENTATION.trim().toLowerCase() !== 'true'
) {
instrumentationConfig['@opentelemetry/instrumentation-fs'] = {
enabled: false,
};
const majorVersionLowerBound = 18;
const majorVersionTestedUpperBound = 22;
const prefix = 'Dash0 OpenTelemtry Distribution';

function init() {
try {
const nodeJsRuntimeVersion = process.version;
const match = nodeJsRuntimeVersion.match(/v(?<majorVersion>\d+)\.(?:\d+)\.(?:\d+)/);
if (!match || !match.groups) {
logProhibitiveError(`Cannot parse Node.js runtime version ${nodeJsRuntimeVersion}.`);
return;
}
const majorVersion = parseInt(match.groups.majorVersion, 10);
if (isNaN(majorVersion)) {
logProhibitiveError(`Cannot parse Node.js runtime version ${nodeJsRuntimeVersion}.`);
return;
}
if (majorVersion < majorVersionLowerBound) {
logProhibitiveError(
`The distribution does not support this Node.js runtime version (${nodeJsRuntimeVersion}). The minimum supported version is Node.js ${majorVersionLowerBound}.0.0.`,
);
return;
}
if (majorVersion > majorVersionTestedUpperBound) {
logWarning(
`Please note: The distribution has not been explicitly tested with this Node.js runtime version (${nodeJsRuntimeVersion}). The maximum tested version is Node.js ${majorVersionTestedUpperBound}.`,
);
}

require('./init');
} catch (e) {
logProhibitiveError(`Initialization failed: ${e}`);
}
}

const configuration: Partial<NodeSDKConfiguration> = {
traceExporter: new OTLPTraceExporter({
url: `${baseUrl}/v1/traces`,
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: `${baseUrl}/v1/metrics`,
}),
}),

instrumentations: [getNodeAutoInstrumentations(instrumentationConfig)],

resource: new Resource({
'telemetry.distro.name': 'dash0-nodejs',
'telemetry.distro.version': version,
}),
};
init();

// Copy the behavior of the NodeSDK constructor with regard to resource detectors, but add the pod uid detector.
// https://github.com/open-telemetry/opentelemetry-js/blob/73fddf9b5e7a93bd4cf21c2dbf444cee31d26c88/experimental/packages/opentelemetry-sdk-node/src/sdk.ts#L126-L132
let detectors: (Detector | DetectorSync)[];
if (process.env.OTEL_NODE_RESOURCE_DETECTORS != null) {
detectors = getResourceDetectorsFromEnv();
} else {
detectors = [envDetector, processDetector, hostDetector];
function logProhibitiveError(message: string) {
console.error(`[${prefix}] ${message} OpenTelemetry data will not be sent to Dash0.`);
}
detectors.push(new PodUidDetector());
detectors.push(new ServiceNameFallbackDetector());
configuration.resourceDetectors = detectors;

const sdk = new NodeSDK(configuration);

sdk.start();

if (process.env.DASH0_DEBUG) {
console.log('Dash0 OpenTelemetry distribution for Node.js: NodeSDK started.');
function logWarning(message: string) {
console.error(`[${prefix}] ${message}`);
}
71 changes: 71 additions & 0 deletions src/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0

import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
import { Detector, DetectorSync, envDetector, hostDetector, processDetector, Resource } from '@opentelemetry/resources';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { NodeSDK, NodeSDKConfiguration } from '@opentelemetry/sdk-node';

import { version } from '../package.json';
import PodUidDetector from './detectors/node/opentelemetry-resource-detector-kubernetes-pod';
import ServiceNameFallbackDetector from './detectors/node/opentelemetry-resource-detector-service-name-fallback';
import { getResourceDetectorsFromEnv } from './util/sdkUtil';

if (process.env.DASH0_DEBUG) {
console.log('Dash0 OpenTelemetry distribution for Node.js: Starting NodeSDK.');
}

let baseUrl = 'http://dash0-opentelemetry-collector-daemonset.default.svc.cluster.local:4318';
if (process.env.DASH0_OTEL_COLLECTOR_BASE_URL) {
baseUrl = process.env.DASH0_OTEL_COLLECTOR_BASE_URL;
}

const instrumentationConfig: any = {};
if (
!process.env.DASH0_ENABLE_FS_INSTRUMENTATION ||
process.env.DASH0_ENABLE_FS_INSTRUMENTATION.trim().toLowerCase() !== 'true'
) {
instrumentationConfig['@opentelemetry/instrumentation-fs'] = {
enabled: false,
};
}

const configuration: Partial<NodeSDKConfiguration> = {
traceExporter: new OTLPTraceExporter({
url: `${baseUrl}/v1/traces`,
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: `${baseUrl}/v1/metrics`,
}),
}),

instrumentations: [getNodeAutoInstrumentations(instrumentationConfig)],

resource: new Resource({
'telemetry.distro.name': 'dash0-nodejs',
'telemetry.distro.version': version,
}),
};

// Copy the behavior of the NodeSDK constructor with regard to resource detectors, but add the pod uid detector.
// https://github.com/open-telemetry/opentelemetry-js/blob/73fddf9b5e7a93bd4cf21c2dbf444cee31d26c88/experimental/packages/opentelemetry-sdk-node/src/sdk.ts#L126-L132
let detectors: (Detector | DetectorSync)[];
if (process.env.OTEL_NODE_RESOURCE_DETECTORS != null) {
detectors = getResourceDetectorsFromEnv();
} else {
detectors = [envDetector, processDetector, hostDetector];
}
detectors.push(new PodUidDetector());
detectors.push(new ServiceNameFallbackDetector());
configuration.resourceDetectors = detectors;

const sdk = new NodeSDK(configuration);

sdk.start();

if (process.env.DASH0_DEBUG) {
console.log('Dash0 OpenTelemetry distribution for Node.js: NodeSDK started.');
}
2 changes: 2 additions & 0 deletions test/.mocharc.integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
module.exports = {
extension: ['ts'],
ignore: ['test/**/node_modules/**'],
// As long as we test the minimum version check with a Node.js version < 18.0.0, we need to enable fetch explicitly.
'node-option': ['experimental-fetch'],
recursive: true,
require: ['ts-node/register'],
slow: 3000,
Expand Down
5 changes: 5 additions & 0 deletions test/collector/CollectorChildProcessWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export default class CollectorChildProcessWrapper extends ChildProcessWrapper {
return stats.traces >= 1;
}

async hasTelemetry() {
const stats = await collector().fetchStats();
return stats.traces >= 1 || stats.metrics >= 1 || stats.logs >= 1;
}

async fetchTelemetry() {
return <OpenTelemetryData>await collector().sendRequest({ command: 'telemetry' });
}
Expand Down
16 changes: 16 additions & 0 deletions test/integration/ChildProcessWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,19 @@ export default class ChildProcessWrapper {
}

class ResponseEmitter extends EventEmitter {}

export function defaultAppConfiguration(appPort: number): ChildProcessWrapperOptions {
return {
path: 'test/apps/express-typescript',
label: 'app',
useTsNode: true,
useDistro: true,
env: {
PORT: appPort.toString(),
// have the Node.js SDK send spans every 100 ms instead of every 5 seconcds to speed up tests
OTEL_BSP_SCHEDULE_DELAY: '100',
DASH0_OTEL_COLLECTOR_BASE_URL: 'http://localhost:4318',
// OTEL_LOG_LEVEL: 'VERBOSE',
},
};
}
55 changes: 55 additions & 0 deletions test/integration/minimumVersion_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0

import { expect } from 'chai';
import semver from 'semver';
import delay from '../util/delay';

import runCommand from '../util/runCommand';
import ChildProcessWrapper, { defaultAppConfiguration } from './ChildProcessWrapper';
import { collector } from './rootHooks';

const { fail } = expect;

const appPort = 1302;

// This test verifies that the distribution does not attempt to initialize itself if the Node.js runtime version is too
// old.
const skipWhenNodeJsVersionIsGreaterOrEquals = '18.0.0';

describe('minimum version check', () => {
let appUnderTest: ChildProcessWrapper;

before(async function () {
if (semver.gte(process.version, skipWhenNodeJsVersionIsGreaterOrEquals)) {
// This suite is deliberatly skipped on supported versions and only run on a Node.js version that is below the
// minimum supported version.
this.skip();
return;
}

await runCommand('npm ci', 'test/apps/express-typescript');
const appConfiguration = defaultAppConfiguration(appPort);
appUnderTest = new ChildProcessWrapper(appConfiguration);
await appUnderTest.start();
});

after(async () => {
if (appUnderTest) {
await appUnderTest.stop();
}
});

it('should stand down without loading the distribution', async () => {
await delay(1000);
const response = await fetch(`http://localhost:${appPort}/ohai`);
await delay(2000);
expect(response.status).to.equal(200);
const responsePayload = await response.json();
expect(responsePayload).to.deep.equal({ message: 'We make Observability easy for every developer.' });

if (await collector().hasTelemetry()) {
fail('The collector received telemetry data although it should not have received anything.');
}
});
});
Loading