Skip to content

Commit

Permalink
chore(instrumentation-undici): add new span assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
david-luna committed Jan 4, 2024
1 parent 4a769eb commit 092b9c1
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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.
*/

/**
* https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md
*/
export enum AttributeNames {
HTTP_ERROR_NAME = 'http.error_name',
HTTP_ERROR_MESSAGE = 'http.error_message',
HTTP_STATUS_TEXT = 'http.status_text',
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,11 @@ export class UndiciInstrumentation extends InstrumentationBase {
if (this._config.enabled) {
return;
}
// This methos is called by the `InstrumentationAbstract` constructor before
// This method is called by the `InstrumentationAbstract` constructor before
// ours is called. So we need to ensure the property is initalized
this._channelSubs = this._channelSubs || [];
this.subscribeToChannel('undici:request:create', this.onRequest.bind(this));
this.subscribeToChannel(
'undici:request:headers',
this.onHeaders.bind(this)
);
this.subscribeToChannel('undici:request:headers',this.onHeaders.bind(this));
this.subscribeToChannel('undici:request:trailers', this.onDone.bind(this));
this.subscribeToChannel('undici:request:error', this.onError.bind(this));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { UndiciInstrumentation } from '../src/undici';

import { MockServer } from './utils/mock-server';
import { assertSpan } from './utils/assertSpan';

const instrumentation = new UndiciInstrumentation();
instrumentation.enable();
Expand All @@ -41,44 +42,41 @@ const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
instrumentation.setTracerProvider(provider);

// Simpler way to skip the while suite
// also `this` is not providing the skpi method inside tests
const shouldTest = typeof globalThis.fetch === 'function'
const describeFn = shouldTest ? describe : describe.skip;

describeFn('UndiciInstrumentation `fetch` tests', () => {
before(done => {
mockServer.start(done);
});

after(done => {
mockServer.stop(done);
});

beforeEach(() => {
memoryExporter.reset();
});

before(() => {
describe('UndiciInstrumentation `fetch` tests', function () {
before(function (done) {
// Do not test if the `fetch` global API is not available
// This applies to nodejs < v18 or nodejs < v16.15 wihtout the flag
// `--experimental-global-fetch` set
// https://nodejs.org/api/globals.html#fetch
if (typeof globalThis.fetch !== 'function') {
this.skip();
}

// TODO: mock propagation and test it
// propagation.setGlobalPropagator(new DummyPropagation());
context.setGlobalContextManager(new AsyncHooksContextManager().enable());
mockServer.start(done);
});

after(() => {
after(function(done) {
context.disable();
propagation.disable();
mockServer.stop(done);
});

describe('enable()', () => {
before(() => {
beforeEach(function () {
memoryExporter.reset();
});

describe('enable()', function () {
before(function () {
instrumentation.enable();
});
after(() => {
after(function () {
instrumentation.disable();
});

it('should create a rootSpan for GET requests and add propagation headers', async () => {
it('should create a rootSpan for GET requests and add propagation headers', async function () {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);

Expand All @@ -99,6 +97,14 @@ describeFn('UndiciInstrumentation `fetch` tests', () => {
[SemanticAttributes.HTTP_STATUS_CODE]: response.status,
[SemanticAttributes.HTTP_TARGET]: '/?query=test',
});
assertSpan(span, {
hostname: 'localhost',
httpStatusCode: response.status,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: response.headers,
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* 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 {
SpanKind,
SpanStatus,
Exception,
SpanStatusCode,
} from '@opentelemetry/api';
import { hrTimeToNanoseconds } from '@opentelemetry/core';
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
// import { DummyPropagation } from './DummyPropagation';
import { AttributeNames } from '../../src/enums/AttributeNames';

export const assertSpan = (
span: ReadableSpan,
validations: {
httpStatusCode?: number;
httpMethod: string;
resHeaders: Headers;
hostname: string;
pathname: string;
reqHeaders?: Headers;
path?: string | null;
forceStatus?: SpanStatus;
serverName?: string;
noNetPeer?: boolean; // we don't expect net peer info when request throw before being sent
error?: Exception;
}
) => {
assert.strictEqual(span.spanContext().traceId.length, 32);
assert.strictEqual(span.spanContext().spanId.length, 16);
assert.strictEqual(span.kind, SpanKind.CLIENT, 'span.kind is correct');
assert.strictEqual(span.name, `HTTP ${validations.httpMethod}`, 'span.name is correct');
assert.strictEqual(
span.attributes[AttributeNames.HTTP_ERROR_MESSAGE],
span.status.message,
`attributes['${AttributeNames.HTTP_ERROR_MESSAGE}'] is correct`,
);
assert.strictEqual(
span.attributes[SemanticAttributes.HTTP_METHOD],
validations.httpMethod,
`attributes['${SemanticAttributes.HTTP_METHOD}'] is correct`,
);
assert.strictEqual(
span.attributes[SemanticAttributes.HTTP_TARGET],
validations.path || validations.pathname,
`attributes['${SemanticAttributes.HTTP_TARGET}'] is correct`,
);
assert.strictEqual(
span.attributes[SemanticAttributes.HTTP_STATUS_CODE],
validations.httpStatusCode
);

assert.strictEqual(span.links.length, 0);

if (validations.error) {
assert.strictEqual(span.events.length, 1);
assert.strictEqual(span.events[0].name, 'exception');

const eventAttributes = span.events[0].attributes;
assert.ok(eventAttributes != null);
assert.deepStrictEqual(Object.keys(eventAttributes), [
'exception.type',
'exception.message',
'exception.stacktrace',
]);
} else {
assert.strictEqual(span.events.length, 0);
}

const { httpStatusCode } = validations;
const isStatusUnset = httpStatusCode && httpStatusCode >= 100 && httpStatusCode < 400;

assert.deepStrictEqual(
span.status,
validations.forceStatus || {
code: isStatusUnset ? SpanStatusCode.UNSET : SpanStatusCode.ERROR
},
);

assert.ok(span.endTime, 'must be finished');
assert.ok(hrTimeToNanoseconds(span.duration) > 0, 'must have positive duration');

const contentLengthHeader = validations.resHeaders.get('content-length');
if (contentLengthHeader) {
const contentLength = Number(contentLengthHeader);

const contentEncodingHeader = validations.resHeaders.get('content-encoding');
if (
contentEncodingHeader &&
contentEncodingHeader !== 'identity'
) {
assert.strictEqual(
span.attributes[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH],
contentLength
);
} else {
assert.strictEqual(
span.attributes[
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED
],
contentLength
);
}
}
assert.strictEqual(
span.attributes[SemanticAttributes.NET_PEER_NAME],
validations.hostname,
'must be consistent (PEER_NAME and hostname)'
);
if (!validations.noNetPeer) {
assert.ok(
span.attributes[SemanticAttributes.NET_PEER_IP],
'must have PEER_IP'
);
assert.ok(
span.attributes[SemanticAttributes.NET_PEER_PORT],
'must have PEER_PORT'
);
}
assert.ok(
(span.attributes[SemanticAttributes.HTTP_URL] as string).indexOf(
span.attributes[SemanticAttributes.NET_PEER_NAME] as string
) > -1,
'must be consistent'
);


if (validations.reqHeaders) {
const userAgent = validations.reqHeaders.get('user-agent');
if (userAgent) {
assert.strictEqual(
span.attributes[SemanticAttributes.HTTP_USER_AGENT],
userAgent
);
}
// assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);
// assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ export class MockServer {

stop(cb: (err?: Error) => void) {
if (this._httpServer) {
this._httpServer.close(cb);
this._httpServer.close();
cb();
}
}
}

0 comments on commit 092b9c1

Please sign in to comment.