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

346 lines
10 KiB
TypeScript

/**
* Copyright 2019, 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 } from '@opentelemetry/base';
import { NoopLogger, hrTimeToMicroseconds } from '@opentelemetry/core';
import * as types from '@opentelemetry/api';
import { Resource } from '@opentelemetry/resources';
import { ZipkinExporter } from '../src';
import * as zipkinTypes from '../src/types';
import { OT_REQUEST_HEADER } from '../src/utils';
import { TraceFlags } from '@opentelemetry/api';
const MICROS_PER_SECS = 1e6;
function getReadableSpan() {
const startTime = 1566156729709;
const duration = 2000;
const readableSpan: ReadableSpan = {
name: 'my-span',
kind: types.SpanKind.INTERNAL,
spanContext: {
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.NONE,
},
startTime: [startTime, 0],
endTime: [startTime + duration, 0],
ended: true,
duration: [duration, 0],
status: {
code: types.CanonicalCode.OK,
},
attributes: {},
links: [],
events: [],
resource: Resource.empty(),
};
return readableSpan;
}
describe('ZipkinExporter', () => {
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',
logger: new NoopLogger(),
});
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',
logger: new NoopLogger(),
});
exporter.export([], (result: ExportResult) => {
assert.strictEqual(result, ExportResult.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: types.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: types.CanonicalCode.OK,
},
attributes: {
key1: 'value1',
key2: 'value2',
},
links: [],
events: [
{
name: 'my-event',
time: [startTime + 10, 0],
attributes: { key3: 'value3' },
},
],
resource: Resource.empty(),
};
const span2: ReadableSpan = {
name: 'my-span',
kind: types.SpanKind.SERVER,
spanContext: {
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
spanId: '6e0c63257de34c92',
traceFlags: TraceFlags.NONE,
},
startTime: [startTime, 0],
endTime: [startTime + duration, 0],
ended: true,
duration: [duration, 0],
status: {
code: types.CanonicalCode.OK,
},
attributes: {},
links: [],
events: [],
resource: Resource.empty(),
};
const exporter = new ZipkinExporter({
serviceName: 'my-service',
logger: new NoopLogger(),
});
exporter.export([span1, span2], (result: ExportResult) => {
scope.done();
assert.strictEqual(result, ExportResult.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',
logger: new NoopLogger(),
url: 'https://localhost:9411/api/v2/spans',
});
exporter.export([getReadableSpan()], (result: ExportResult) => {
scope.done();
assert.strictEqual(result, ExportResult.SUCCESS);
});
});
it(`should send '${OT_REQUEST_HEADER}' header`, () => {
const scope = nock('https://localhost:9411')
.post('/api/v2/spans')
.reply(function(uri, requestBody, cb) {
assert.ok(this.req.headers[OT_REQUEST_HEADER]);
cb(null, [200, 'Ok']);
});
const exporter = new ZipkinExporter({
serviceName: 'my-service',
logger: new NoopLogger(),
url: 'https://localhost:9411/api/v2/spans',
});
exporter.export([getReadableSpan()], (result: ExportResult) => {
scope.done();
assert.strictEqual(result, ExportResult.SUCCESS);
});
});
it('should return FailedNonRetryable with 4xx', () => {
const scope = nock('http://localhost:9411')
.post('/api/v2/spans')
.reply(400);
const exporter = new ZipkinExporter({
serviceName: 'my-service',
logger: new NoopLogger(),
});
exporter.export([getReadableSpan()], (result: ExportResult) => {
scope.done();
assert.strictEqual(result, ExportResult.FAILED_NOT_RETRYABLE);
});
});
it('should return FailedRetryable with 5xx', () => {
const scope = nock('http://localhost:9411')
.post('/api/v2/spans')
.reply(500);
const exporter = new ZipkinExporter({
serviceName: 'my-service',
logger: new NoopLogger(),
});
exporter.export([getReadableSpan()], (result: ExportResult) => {
scope.done();
assert.strictEqual(result, ExportResult.FAILED_RETRYABLE);
});
});
it('should return FailedRetryable 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',
logger: new NoopLogger(),
});
exporter.export([getReadableSpan()], (result: ExportResult) => {
scope.done();
assert.strictEqual(result, ExportResult.FAILED_RETRYABLE);
});
});
it('should return FailedNonRetryable after shutdown', done => {
const exporter = new ZipkinExporter({
serviceName: 'my-service',
logger: new NoopLogger(),
});
exporter.shutdown();
exporter.export([getReadableSpan()], (result: ExportResult) => {
assert.strictEqual(result, ExportResult.FAILED_NOT_RETRYABLE);
done();
});
});
});
describe('shutdown', () => {
before(() => {
nock.disableNetConnect();
});
after(() => {
nock.enableNetConnect();
});
// @todo: implement
it('should send by default');
});
});