opentelemetry-js/packages/opentelemetry-exporter-zipkin/test/node/zipkin.test.ts

533 lines
15 KiB
TypeScript

/*
* 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 * as assert from 'assert';
import * as nock from 'nock';
import { ReadableSpan } from '@opentelemetry/tracing';
import {
ExportResult,
hrTimeToMicroseconds,
ExportResultCode,
} from '@opentelemetry/core';
import * as api from '@opentelemetry/api';
import { Resource } from '@opentelemetry/resources';
import { ZipkinExporter } from '../../src';
import * as zipkinTypes from '../../src/types';
import { TraceFlags } from '@opentelemetry/api';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
const MICROS_PER_SECS = 1e6;
function getReadableSpan() {
const startTime = 1566156729709;
const duration = 2000;
const readableSpan: ReadableSpan = {
name: 'my-span',
kind: api.SpanKind.INTERNAL,
spanContext: () => {
return {
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.NONE,
};
},
startTime: [startTime, 0],
endTime: [startTime + duration, 0],
ended: true,
duration: [duration, 0],
status: {
code: api.SpanStatusCode.OK,
},
attributes: {},
links: [],
events: [],
resource: Resource.empty(),
instrumentationLibrary: { name: 'default', version: '0.0.1' },
};
return readableSpan;
}
describe('Zipkin Exporter - node', () => {
describe('constructor', () => {
it('should construct an exporter', () => {
const exporter = new ZipkinExporter({ serviceName: 'my-service' });
assert.ok(typeof exporter.export === 'function');
assert.ok(typeof exporter.shutdown === 'function');
});
it('should construct an exporter with url', () => {
const exporter = new ZipkinExporter({
serviceName: 'my-service',
url: 'http://localhost',
});
assert.ok(typeof exporter.export === 'function');
assert.ok(typeof exporter.shutdown === 'function');
});
it('should construct an exporter with logger', () => {
const exporter = new ZipkinExporter({
serviceName: 'my-service',
});
assert.ok(typeof exporter.export === 'function');
assert.ok(typeof exporter.shutdown === 'function');
});
it('should construct an exporter with statusCodeTagName', () => {
const exporter = new ZipkinExporter({
serviceName: 'my-service',
statusCodeTagName: 'code',
});
assert.ok(typeof exporter.export === 'function');
assert.ok(typeof exporter.shutdown === 'function');
});
it('should construct an exporter with statusDescriptionTagName', () => {
const exporter = new ZipkinExporter({
serviceName: 'my-service',
statusDescriptionTagName: 'description',
});
assert.ok(typeof exporter.export === 'function');
assert.ok(typeof exporter.shutdown === 'function');
});
});
describe('export', () => {
before(() => {
nock.disableNetConnect();
});
after(() => {
nock.enableNetConnect();
});
it('should skip send with empty array', () => {
const exporter = new ZipkinExporter({
serviceName: 'my-service',
});
exporter.export([], (result: ExportResult) => {
assert.strictEqual(result.code, ExportResultCode.SUCCESS);
});
});
it('should send spans to Zipkin backend and return with Success', () => {
let requestBody: [zipkinTypes.Span];
const scope = nock('http://localhost:9411')
.post('/api/v2/spans', (body: [zipkinTypes.Span]) => {
requestBody = body;
return true;
})
.reply(202);
const parentSpanId = '5c1c63257de34c67';
const startTime = 1566156729709;
const duration = 2000;
const span1: ReadableSpan = {
name: 'my-span',
kind: api.SpanKind.INTERNAL,
parentSpanId,
spanContext: () => {
return {
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.NONE,
};
},
startTime: [startTime, 0],
endTime: [startTime + duration, 0],
ended: true,
duration: [duration, 0],
status: {
code: api.SpanStatusCode.OK,
},
attributes: {
key1: 'value1',
key2: 'value2',
},
links: [],
events: [
{
name: 'my-event',
time: [startTime + 10, 0],
attributes: { key3: 'value3' },
},
],
resource: Resource.empty(),
instrumentationLibrary: { name: 'default', version: '0.0.1' },
};
const span2: ReadableSpan = {
name: 'my-span',
kind: api.SpanKind.SERVER,
spanContext: () => {
return {
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.NONE,
};
},
startTime: [startTime, 0],
endTime: [startTime + duration, 0],
ended: true,
duration: [duration, 0],
status: {
code: api.SpanStatusCode.OK,
},
attributes: {},
links: [],
events: [],
resource: Resource.empty(),
instrumentationLibrary: { name: 'default', version: '0.0.1' },
};
const exporter = new ZipkinExporter({
serviceName: 'my-service',
});
exporter.export([span1, span2], (result: ExportResult) => {
scope.done();
assert.strictEqual(result.code, ExportResultCode.SUCCESS);
assert.deepStrictEqual(requestBody, [
// Span 1
{
annotations: [
{
value: 'my-event',
timestamp: (startTime + 10) * MICROS_PER_SECS,
},
],
duration: duration * MICROS_PER_SECS,
id: span1.spanContext().spanId,
localEndpoint: {
serviceName: 'my-service',
},
name: span1.name,
parentId: parentSpanId,
tags: {
key1: 'value1',
key2: 'value2',
'ot.status_code': 'OK',
},
timestamp: startTime * MICROS_PER_SECS,
traceId: span1.spanContext().traceId,
},
// Span 2
{
duration: duration * MICROS_PER_SECS,
id: span2.spanContext().spanId,
kind: 'SERVER',
localEndpoint: {
serviceName: 'my-service',
},
name: span2.name,
tags: {
'ot.status_code': 'OK',
},
timestamp: hrTimeToMicroseconds([startTime, 0]),
traceId: span2.spanContext().traceId,
},
]);
});
});
it('should support https protocol', () => {
const scope = nock('https://localhost:9411')
.post('/api/v2/spans')
.reply(200);
const exporter = new ZipkinExporter({
serviceName: 'my-service',
url: 'https://localhost:9411/api/v2/spans',
});
exporter.export([getReadableSpan()], (result: ExportResult) => {
scope.done();
assert.strictEqual(result.code, ExportResultCode.SUCCESS);
});
});
it('should return Failed result with 4xx', () => {
const scope = nock('http://localhost:9411')
.post('/api/v2/spans')
.reply(400);
const exporter = new ZipkinExporter({
serviceName: 'my-service',
});
exporter.export([getReadableSpan()], (result: ExportResult) => {
scope.done();
assert.strictEqual(result.code, ExportResultCode.FAILED);
});
});
it('should return failed result with 5xx', () => {
const scope = nock('http://localhost:9411')
.post('/api/v2/spans')
.reply(500);
const exporter = new ZipkinExporter({
serviceName: 'my-service',
});
exporter.export([getReadableSpan()], (result: ExportResult) => {
scope.done();
assert.strictEqual(result.code, ExportResultCode.FAILED);
});
});
it('should return failed result with socket error', () => {
const scope = nock('http://localhost:9411')
.post('/api/v2/spans')
.replyWithError(new Error('My Socket Error'));
const exporter = new ZipkinExporter({
serviceName: 'my-service',
});
exporter.export([getReadableSpan()], (result: ExportResult) => {
scope.done();
assert.strictEqual(result.code, ExportResultCode.FAILED);
});
});
it('should return failed result after shutdown', done => {
const exporter = new ZipkinExporter({
serviceName: 'my-service',
});
exporter.shutdown();
exporter.export([getReadableSpan()], (result: ExportResult) => {
assert.strictEqual(result.code, ExportResultCode.FAILED);
done();
});
});
it('should call globalErrorHandler on error', () => {
const expectedError = new Error('Whoops');
const scope = nock('http://localhost:9411')
.post('/api/v2/spans')
.replyWithError(expectedError);
const exporter = new ZipkinExporter({
serviceName: 'my-service',
});
exporter.export([getReadableSpan()], (result: ExportResult) => {
assert.deepStrictEqual(result.code, ExportResultCode.FAILED);
assert.deepStrictEqual(result.error, expectedError);
scope.done();
});
});
});
it('should set serviceName per-span if resource has one', () => {
const resource_service_name = 'resource_service_name';
const resource_service_name_prime = 'resource_service_name_prime';
let requestBody: zipkinTypes.Span[];
const scope = nock('http://localhost:9411')
.post('/api/v2/spans', body => {
requestBody = body;
return true;
})
.replyWithError(new Error('My Socket Error'));
const parentSpanId = '5c1c63257de34c67';
const startTime = 1566156729709;
const duration = 2000;
const span1: ReadableSpan = {
name: 'my-span',
kind: api.SpanKind.INTERNAL,
parentSpanId,
spanContext: () => ({
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.NONE,
}),
startTime: [startTime, 0],
endTime: [startTime + duration, 0],
ended: true,
duration: [duration, 0],
status: {
code: api.SpanStatusCode.OK,
},
attributes: {
key1: 'value1',
key2: 'value2',
},
links: [],
events: [
{
name: 'my-event',
time: [startTime + 10, 0],
attributes: { key3: 'value3' },
},
],
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: resource_service_name,
}),
instrumentationLibrary: { name: 'default', version: '0.0.1' },
};
const span2: ReadableSpan = {
name: 'my-span',
kind: api.SpanKind.SERVER,
spanContext: () => ({
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.NONE,
}),
startTime: [startTime, 0],
endTime: [startTime + duration, 0],
ended: true,
duration: [duration, 0],
status: {
code: api.SpanStatusCode.OK,
},
attributes: {},
links: [],
events: [],
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: resource_service_name_prime,
}),
instrumentationLibrary: { name: 'default', version: '0.0.1' },
};
const exporter = new ZipkinExporter({});
exporter.export([span1, span2], (result: ExportResult) => {
requestBody;
scope.done();
assert.equal(
requestBody[0].localEndpoint.serviceName,
resource_service_name
);
assert.equal(
requestBody[1].localEndpoint.serviceName,
resource_service_name_prime
);
});
});
it('should set serviceName per-span if span has attribute', () => {
const span_service_name = 'span_service_name';
const span_service_name_prime = 'span_service_name_prime';
let requestBody: any;
const scope = nock('http://localhost:9411')
.post('/api/v2/spans', body => {
requestBody = body;
return true;
})
.replyWithError(new Error('My Socket Error'));
const parentSpanId = '5c1c63257de34c67';
const startTime = 1566156729709;
const duration = 2000;
const span1: ReadableSpan = {
name: 'my-span',
kind: api.SpanKind.INTERNAL,
parentSpanId,
spanContext: () => ({
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.NONE,
}),
startTime: [startTime, 0],
endTime: [startTime + duration, 0],
ended: true,
duration: [duration, 0],
status: {
code: api.SpanStatusCode.OK,
},
attributes: {
key1: 'value1',
key2: 'value2',
[SemanticResourceAttributes.SERVICE_NAME]: span_service_name,
},
links: [],
events: [
{
name: 'my-event',
time: [startTime + 10, 0],
attributes: { key3: 'value3' },
},
],
resource: Resource.empty(),
instrumentationLibrary: { name: 'default', version: '0.0.1' },
};
const span2: ReadableSpan = {
name: 'my-span',
kind: api.SpanKind.SERVER,
spanContext: () => ({
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.NONE,
}),
startTime: [startTime, 0],
endTime: [startTime + duration, 0],
ended: true,
duration: [duration, 0],
status: {
code: api.SpanStatusCode.OK,
},
attributes: {
[SemanticResourceAttributes.SERVICE_NAME]: span_service_name_prime,
},
links: [],
events: [],
resource: Resource.empty(),
instrumentationLibrary: { name: 'default', version: '0.0.1' },
};
const exporter = new ZipkinExporter({});
exporter.export([span1, span2], (result: ExportResult) => {
requestBody;
scope.done();
assert.equal(
requestBody[0].localEndpoint.serviceName,
span_service_name
);
assert.equal(
requestBody[1].localEndpoint.serviceName,
span_service_name_prime
);
});
});
describe('when env.OTEL_EXPORTER_ZIPKIN_ENDPOINT is set', () => {
before(() => {
process.env.OTEL_EXPORTER_ZIPKIN_ENDPOINT = 'http://localhost:9412';
})
after(() => {
delete process.env.OTEL_EXPORTER_ZIPKIN_ENDPOINT
})
it('should use url from env', () => {
const scope = nock('http://localhost:9412').post('/').reply(200);
const exporter = new ZipkinExporter({
serviceName: 'my-service',
});
exporter.export([getReadableSpan()], (result: ExportResult) => {
scope.done();
assert.strictEqual(result.code, ExportResultCode.SUCCESS);
});
});
});
});