Skip to content

Commit

Permalink
feat: derive a fallback for service.name if not set
Browse files Browse the repository at this point in the history
by finding the application's main package.json file and using its name
and version attribute.
  • Loading branch information
basti1302 committed May 14, 2024
1 parent 65eb805 commit 41d0829
Show file tree
Hide file tree
Showing 13 changed files with 822 additions and 17 deletions.
21 changes: 17 additions & 4 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 @@ -71,6 +71,7 @@
"sinon": "^17.0.1",
"ts-node": "^10.9.2",
"ts-proto": "^1.172.0",
"type-fest": "^4.18.2",
"typescript": "^5.4.5",
"typescript-eslint": "^7.7.1"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const procSelfMountInfoContentWithPodUid2 = `2317 1432 0:473 / / rw,relatime mas
1456 2318 0:476 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
1457 2322 0:480 / /sys/firmware ro,relatime - tmpfs tmpfs ro`;

describe('podUidDetector', () => {
describe('pod ui detection', () => {
const sandbox = sinon.createSandbox();
let readFileStub: sinon.SinonStub;
let podUidDetector: PodUidDetector;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0

import { DetectorSync, Resource } from '@opentelemetry/resources';
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';

import { readPackageJson } from './packageJsonUtil';

export default class ServiceNameFallbackDetector implements DetectorSync {
detect(): Resource {
return new Resource({}, this.detectServiceNameFallback());
}

private async detectServiceNameFallback(): Promise<any> {
if (
hasOptedOutOfServiceNameFallbackDetection() ||
hasOTelServiceNameSet() ||
hasServiceNameSetViaResourceAttributesThing()
) {
return {};
}

const packageJson = await readPackageJson();
if (!packageJson) {
return {};
}
return { [SEMRESATTRS_SERVICE_NAME]: `${packageJson.name}@${packageJson.version}` };
}
}

function hasOptedOutOfServiceNameFallbackDetection() {
const automaticServiceName = process.env.DASH0_AUTOMATIC_SERVICE_NAME;
return automaticServiceName && automaticServiceName.trim().toLowerCase() === 'false';
}

function hasOTelServiceNameSet() {
const otelServiceName = process.env.OTEL_SERVICE_NAME;
return otelServiceName && otelServiceName.trim() !== '';
}

function hasServiceNameSetViaResourceAttributesThing() {
const otelResourceAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES;
if (otelResourceAttributes) {
const rawAttributes: string[] = otelResourceAttributes.split(',');
for (const rawAttribute of rawAttributes) {
const keyValuePair: string[] = rawAttribute.split('=');
if (keyValuePair.length !== 2) {
continue;
}
const [key, value] = keyValuePair;
if (key.trim() === SEMRESATTRS_SERVICE_NAME) {
if (value != null && value.trim().split(/^"|"$/).join('').trim().length > 0) {
return true;
}
}
}
}
return false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0

import { Resource } from '@opentelemetry/resources';
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
import { expect } from 'chai';
import Sinon from 'sinon';
import sinon from 'sinon';

import ServiceNameFallbackDetector from './index';
import * as packageJsonUtil from './packageJsonUtil';

const packageJson = {
name: '@example/app-under-test',
version: '2.13.47',
description: 'a Node.js application',
main: 'src/index.js',
};

const envVarNames = [
//
'DASH0_AUTOMATIC_SERVICE_NAME',
'OTEL_SERVICE_NAME',
'OTEL_RESOURCE_ATTRIBUTES',
];

interface Dict<T> {
[key: string]: T | undefined;
}

describe('service name fallback', () => {
const sandbox = sinon.createSandbox();
let readPackageJsonStub: Sinon.SinonStub;
let serviceNameFallback: ServiceNameFallbackDetector;

const originalEnvVarValues: Dict<string> = {};

before(() => {
envVarNames.forEach(envVarName => {
originalEnvVarValues[envVarName] = process.env[envVarName];
});
});

beforeEach(() => {
readPackageJsonStub = sandbox.stub(packageJsonUtil, 'readPackageJson');
serviceNameFallback = new ServiceNameFallbackDetector();
});

afterEach(() => {
sandbox.restore();
envVarNames.forEach(envVarName => {
if (originalEnvVarValues[envVarName] === undefined) {
delete process.env[envVarName];
} else {
process.env[envVarName] = originalEnvVarValues[envVarName];
}
});
});

it('sets a service name based on package.json attributes', async () => {
givenAValidPackageJsonFile();
const result = serviceNameFallback.detect();
const attributes = await waitForAsyncDetection(result);
expect(attributes).to.have.property(SEMRESATTRS_SERVICE_NAME, '@example/[email protected]');
});

it('does not set a service name if DASH0_AUTOMATIC_SERVICE_NAME is false', async () => {
givenAValidPackageJsonFile();
process.env.DASH0_AUTOMATIC_SERVICE_NAME = 'false';
const result = serviceNameFallback.detect();
const attributes = await waitForAsyncDetection(result);
expect(attributes).to.be.empty;
});

it('does not set a service name if OTEL_SERVICE_NAME is set', async () => {
givenAValidPackageJsonFile();
process.env.OTEL_SERVICE_NAME = 'already-set';
const result = serviceNameFallback.detect();
const attributes = await waitForAsyncDetection(result);
expect(attributes).to.be.empty;
});

it('sets a service name if OTEL_SERVICE_NAME is set to an empty string', async () => {
givenAValidPackageJsonFile();
process.env.OTEL_SERVICE_NAME = ' ';
const result = serviceNameFallback.detect();
const attributes = await waitForAsyncDetection(result);
expect(attributes).to.have.property(SEMRESATTRS_SERVICE_NAME, '@example/[email protected]');
});

it('does not set a service name if OTEL_RESOURCE_ATTRIBUTES has the service.name key', async () => {
givenAValidPackageJsonFile();
process.env.OTEL_RESOURCE_ATTRIBUTES = 'key1=value,service.name=already-set,key2=valu,key2=valuee';
const result = serviceNameFallback.detect();
const attributes = await waitForAsyncDetection(result);
expect(attributes).to.be.empty;
});

it('sets a service name if OTEL_RESOURCE_ATTRIBUTES is set but does not have the service.name key', async () => {
givenAValidPackageJsonFile();
process.env.OTEL_RESOURCE_ATTRIBUTES = 'key1=value,key2=value';
const result = serviceNameFallback.detect();
const attributes = await waitForAsyncDetection(result);
expect(attributes).to.have.property(SEMRESATTRS_SERVICE_NAME, '@example/[email protected]');
});

it('does not set a service name if no package.json can be found', async () => {
givenThereIsNoPackageJsonFile();
const result = serviceNameFallback.detect();
const attributes = await waitForAsyncDetection(result);
expect(attributes).to.be.empty;
});

function givenThereIsNoPackageJsonFile() {
readPackageJsonStub.returns(null);
}

function givenAValidPackageJsonFile() {
readPackageJsonStub.returns(null).returns(packageJson);
}

async function waitForAsyncDetection(result: Resource) {
expect(result).to.exist;
expect(result.asyncAttributesPending).to.be.true;
// @ts-expect-error required
await result.waitForAsyncAttributes();
return result.attributes;
}
});
Loading

0 comments on commit 41d0829

Please sign in to comment.