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: add detector for container ID in cgroup v1 #515

Merged
merged 20 commits into from
Sep 16, 2022
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add detector for container ID in cgroup v1",
"packageName": "@splunk/otel",
"email": "rauno56@gmail.com",
"dependentChangeType": "patch"
}
135 changes: 135 additions & 0 deletions src/detectors/DockerCGroupV1Detector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright Splunk Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/* This is based on a detector from OTel https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-resources/src/detectors/
We're copying this code and changing the implementation to a synchronous one from async. This is required for our distribution to not incur ~1 second of overhead
when setting up the tracing pipeline. This is a temporary solution until we can agree upon and implement a solution upstream.
*/

/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Resource, ResourceDetectionConfig } from '@opentelemetry/resources';

import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';

import * as fs from 'fs';
import { platform } from 'os';
import { diag } from '@opentelemetry/api';

const isValidBase16String = (hexString: string) => {
for (let ch = 0; ch < hexString.length; ch++) {
const code = hexString.charCodeAt(ch);
if (
(48 <= code && code <= 57) ||
(97 <= code && code <= 102) ||
(65 <= code && code <= 70)
) {
continue;
}
return false;
}
return true;
};

export class DockerCGroupV1Detector {
public detect(_config?: ResourceDetectionConfig): Resource {
if (platform() !== 'linux') {
diag.debug('Docker CGROUP V1 Detector skipped: Not on linux');
return Resource.empty();
}
try {
const containerId = this._getContainerId();
return !containerId
? Resource.empty()
: new Resource({
[SemanticResourceAttributes.CONTAINER_ID]: containerId,
});
} catch (e) {
diag.info(
'Docker CGROUP V1 Detector did not identify running inside a supported docker container, no docker attributes will be added to resource: ',
e
);
return Resource.empty();
}
}

protected _getContainerId(): string | null {
try {
const rawData = fs.readFileSync('/proc/self/cgroup', 'utf8').trim();
return this._parseFile(rawData);
} catch (e) {
if (e instanceof Error) {
const errorMessage = e.message;
diag.info(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this mean logging this error on other platforms besides Linux?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, with the current implementation it would. Your suggestion?

How about we detect the platform and only call _getContainerId() if it's linux?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the whole detector could check for process.platform === 'linux'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean by "whole". Since it's currently the only platform-specific detector, added it into the detector itself. Once we have more, we could do it in resource.ts to optimize.

'Docker CGROUP V1 Detector failed to read the Container ID: ',
errorMessage
);
}
}
return null;
}

/*
This is very likely has false positives since it does not check for the ID length,
but is very robust in usually finding the right thing, and if not, finding some
identifier for differentiating between containers.
It also matches Java: https://github.com/open-telemetry/opentelemetry-java/commit/2cb461d4aef16f1ac1c5e67edc2fb41f90ed96a3#diff-ad68bc34d4da31a50709591d4b7735f88c008be7ed1fc325c6367dd9df033452
*/
protected _parseFile(contents: string): string | null {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not important / nit, but _parseFile does not actually parse a file, but a container id? 💭

if (typeof contents !== 'string') {
return null;
}
for (const line of contents.split('\n')) {
const lastSlashIdx = line.lastIndexOf('/');
if (lastSlashIdx < 0) {
return null;
}

const lastSection = line.substring(lastSlashIdx + 1);
let startIdx = lastSection.lastIndexOf('-');
let endIdx = lastSection.lastIndexOf('.');

startIdx = startIdx === -1 ? 0 : startIdx + 1;
if (endIdx === -1) {
endIdx = lastSection.length;
}
if (startIdx > endIdx) {
return null;
}

const containerId = lastSection.substring(startIdx, endIdx);
if (containerId && isValidBase16String(containerId)) {
return containerId;
}
}
return null;
}
}

export const dockerCGroupV1Detector = new DockerCGroupV1Detector();
2 changes: 2 additions & 0 deletions src/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import { diag } from '@opentelemetry/api';
import { Resource } from '@opentelemetry/resources';

import { distroDetector } from './detectors/DistroDetector';
import { dockerCGroupV1Detector } from './detectors/DockerCGroupV1Detector';
import { envDetector } from './detectors/EnvDetector';
import { hostDetector } from './detectors/HostDetector';
import { osDetector } from './detectors/OSDetector';
import { processDetector } from './detectors/ProcessDetector';

const detectors = [
distroDetector,
dockerCGroupV1Detector,
envDetector,
hostDetector,
osDetector,
Expand Down
44 changes: 39 additions & 5 deletions test/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import {
InMemorySpanExporter,
} from '@opentelemetry/sdk-trace-base';

import * as assert from 'assert';
import { strict as assert } from 'assert';
import * as sinon from 'sinon';
import * as fs from 'fs';
import * as os from 'os';

import * as instrumentations from '../src/instrumentations';
import {
Expand All @@ -40,6 +42,20 @@ import {
} from '../src/tracing/options';
import * as utils from './utils';

const assertVersion = versionAttr => {
assert.equal(typeof versionAttr, 'string');
assert(
/[0-9]+\.[0-9]+\.[0-9]+/.test(versionAttr),
`${versionAttr} is not a valid version`
);
};
const assertContainerId = containerIdAttr => {
assert.equal(typeof containerIdAttr, 'string');
assert(
/^[abcdef0-9]+$/i.test(containerIdAttr),
`${containerIdAttr} is not an hex string`
);
};
/*
service.name attribute is not set, your service is unnamed and will be difficult to identify.
Set your service name using the OTEL_RESOURCE_ATTRIBUTES environment variable.
Expand All @@ -52,6 +68,7 @@ const MATCH_NO_INSTRUMENTATIONS_WARNING = sinon.match(
);
// List of resource attributes we expect to see detected
const expectedAttributes = new Set([
SemanticResourceAttributes.CONTAINER_ID,
SemanticResourceAttributes.HOST_ARCH,
SemanticResourceAttributes.HOST_NAME,
SemanticResourceAttributes.OS_TYPE,
Expand All @@ -69,6 +86,8 @@ const expectedAttributes = new Set([

describe('options', () => {
let logger;
let readFileSyncStub;
let platformStub;

beforeEach(utils.cleanEnvironment);

Expand All @@ -79,10 +98,21 @@ describe('options', () => {
api.diag.setLogger(logger, api.DiagLogLevel.ALL);
// Setting logger logs stuff. Cleaning that up.
logger.warn.resetHistory();

// as long as it doesn't conflict with other tests, we can stub platform for linux to test cgroup v1 detector
platformStub = sinon.stub(os, 'platform').returns('linux');
readFileSyncStub = sinon.stub(fs, 'readFileSync');
readFileSyncStub
.withArgs('/proc/self/cgroup', 'utf8')
.returns(
'1:blkio:/docker/a4d00c9dd675d67f866c786181419e1b44832d4696780152e61afd44a3e02856\n'
);
});

afterEach(() => {
api.diag.disable();
platformStub.restore();
readFileSyncStub.restore();
});

describe('defaults', () => {
Expand All @@ -101,10 +131,14 @@ describe('options', () => {
it('has expected defaults', () => {
const options = _setDefaultOptions();

assert(
/[0-9]+\.[0-9]+\.[0-9]+/.test(
options.tracerConfig.resource.attributes['splunk.distro.version']
)
assertVersion(
options.tracerConfig.resource.attributes['splunk.distro.version']
);

assertContainerId(
options.tracerConfig.resource.attributes[
SemanticResourceAttributes.CONTAINER_ID
]
);

// resource attributes for process, host and os are different at each run, iterate through them, make sure they exist and then delete
Expand Down
56 changes: 56 additions & 0 deletions test/resource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as assert from 'assert';

import * as otel from '@opentelemetry/api';
import { EnvDetector } from '../src/detectors/EnvDetector';
import { DockerCGroupV1Detector } from '../src/detectors/DockerCGroupV1Detector';
import { detect } from '../src/resource';
import * as utils from './utils';

Expand Down Expand Up @@ -62,6 +63,61 @@ describe('resource detector', () => {
});
});

describe('DockerCGroupV1Detector', () => {
const invalidCases = [
'13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23zzzz',
];
const expectedId = 'ac679f8a8319c8cf7d38e1adf263bc08d23';
const testCases = [
[
'13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23.slice',
'ac679f8a8319c8cf7d38e1adf263bc08d23',
],
[
'13:name=systemd:/podruntime/docker/kubepods/crio-dc679f8a8319c8cf7d38e1adf263bc08d23.slice',
'dc679f8a8319c8cf7d38e1adf263bc08d23',
],
[
'13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356',
'd86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356',
],
[
[
'1:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23',
'2:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23',
'3:name=systemd:/podruntime/docker/kubepods/docker-dc579f8a8319c8cf7d38e1adf263bc08d23',
].join('\n'),
'dc579f8a8319c8cf7d38e1adf263bc08d23',
],
[
[
'1:blkio:/docker/a4d00c9dd675d67f866c786181419e1b44832d4696780152e61afd44a3e02856',
'2:cpu:/docker/a4d00c9dd675d67f866c786181419e1b44832d4696780152e61afd44a3e02856',
'3:cpuacct:/docker/a4d00c9dd675d67f866c786181419e1b44832d4696780152e61afd44a3e02856',
].join('\n'),
'a4d00c9dd675d67f866c786181419e1b44832d4696780152e61afd44a3e02856',
],
[
[
'1:blkio:/ecs/eb9d3d0c-8936-42d7-80d8-f82b2f1a629e/7e9139716d9e5d762d22f9f877b87d1be8b1449ac912c025a984750c5dbff157',
'2:cpu:/ecs/eb9d3d0c-8936-42d7-80d8-f82b2f1a629e/7e9139716d9e5d762d22f9f877b87d1be8b1449ac912c025a984750c5dbff157',
'3:cpuacct:/ecs/eb9d3d0c-8936-42d7-80d8-f82b2f1a629e/7e9139716d9e5d762d22f9f877b87d1be8b1449ac912c025a984750c5dbff157',
].join('\n'),
'7e9139716d9e5d762d22f9f877b87d1be8b1449ac912c025a984750c5dbff157',
],
];

it('parses all the known test cases correctly', () => {
const detector = new DockerCGroupV1Detector();
testCases.forEach(([testCase, result]) => {
assert.equal(detector['_parseFile'](testCase), result);
});
invalidCases.forEach(([testCase, result]) => {
assert.equal(detector['_parseFile'](testCase), null);
});
});
});

describe('resource.detect', () => {
it('catches resource attributes from the env', () => {
process.env.OTEL_RESOURCE_ATTRIBUTES = 'k=v,service.name=node-svc';
Expand Down