Skip to content

Commit

Permalink
test(zipkin): basic tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter Marton committed Aug 17, 2019
1 parent ad211ef commit 945cfe1
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 35 deletions.
3 changes: 3 additions & 0 deletions packages/opentelemetry-exporter-zipkin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@
"access": "public"
},
"devDependencies": {
"@opentelemetry/scope-base": "^0.0.1",
"@types/mocha": "^5.2.7",
"@types/nock": "^10.0.3",
"@types/node": "^12.6.9",
"codecov": "^3.5.0",
"gts": "^1.1.0",
"mocha": "^6.2.0",
"nock": "^10.0.6",
"nyc": "^14.1.1",
"ts-mocha": "^6.0.0",
"ts-node": "^8.3.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/opentelemetry-exporter-zipkin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export * from './zipkin';
41 changes: 23 additions & 18 deletions packages/opentelemetry-exporter-zipkin/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,27 @@ import { ReadableSpan } from '@opentelemetry/basic-tracer';
import * as zipkinTypes from './types';

const MICROS_PER_MILLI = 1000;

// TODO: what should be the mapping?
const STATUS_CODE = 'ot.status_code';
const STATUS_DESCRIPTION = 'ot.status_description';

const ZIPKIN_SPAN_KIND_MAPPING = {
[types.SpanKind.CLIENT]: zipkinTypes.SpanKind.CLIENT,
[types.SpanKind.SERVER]: zipkinTypes.SpanKind.SERVER,
[types.SpanKind.CONSUMER]: zipkinTypes.SpanKind.CONSUMER,
[types.SpanKind.PRODUCER]: zipkinTypes.SpanKind.PRODUCER,
// TODO: discuss mapping
// Zipkin doesnt have type INTERNAL
[types.SpanKind.INTERNAL]: zipkinTypes.SpanKind.CLIENT,
// When absent, the span is local.
[types.SpanKind.INTERNAL]: undefined,
};

export const STATUS_CODE_TAG_NAME = 'ot.status_code';
export const STATUS_DESCRIPTION_TAG_NAME = 'ot.status_description';

/**
* Translate OpenTelemetry ReadableSpan to ZipkinSpan format
* @param span Span to be translated
*/
export function toZipkinSpan(
span: ReadableSpan,
serviceName: string,
span: ReadableSpan
statusCodeTagName: string,
statusDescriptionTagName: string
): zipkinTypes.Span {
const zipkinSpan: zipkinTypes.Span = {
traceId: span.spanContext.traceId,
Expand All @@ -49,13 +48,18 @@ export function toZipkinSpan(
id: span.spanContext.spanId,
kind: ZIPKIN_SPAN_KIND_MAPPING[span.kind],
timestamp: span.startTime * MICROS_PER_MILLI,
duration: (span.startTime - span.endTime) * MICROS_PER_MILLI,
duration: (span.endTime - span.startTime) * MICROS_PER_MILLI,
debug: true,
// FIXME: how to determinate that it was created from existing span context?
// @todo: how to determinate that it was created from existing span context?
// True if we are contributing to a span started by another tracer (ex on a different host).
shared: false,
localEndpoint: { serviceName },
tags: _toZipkinTags(span.attributes, span.status),
tags: _toZipkinTags(
span.attributes,
span.status,
statusCodeTagName,
statusDescriptionTagName
),
annotations: _toZipkinAnnotations(span.events),
};

Expand All @@ -65,15 +69,17 @@ export function toZipkinSpan(
/** Converts OpenTelemetry Attributes and Status to Zipkin Tags format. */
export function _toZipkinTags(
attributes: types.Attributes,
status: types.Status
status: types.Status,
statusCodeTagName: string,
statusDescriptionTagName: string
): zipkinTypes.Tags {
const tags: { [key: string]: string } = {};
for (const key of Object.keys(attributes)) {
tags[key] = String(attributes[key]);
}
tags[STATUS_CODE] = String(status.code);
tags[STATUS_CODE_TAG_NAME] = String(status.code);
if (status.message) {
tags[STATUS_DESCRIPTION] = status.message;
tags[STATUS_DESCRIPTION_TAG_NAME] = status.message;
}
return tags;
}
Expand All @@ -82,11 +88,10 @@ export function _toZipkinTags(
* Converts OpenTelemetry Events to Zipkin Annotations format.
*/
export function _toZipkinAnnotations(
events: types.Event[]
events: types.TimedEvent[]
): zipkinTypes.Annotation[] {
return events.map(event => ({
// FIXME: use event timestamp
timestamp: Date.now() * MICROS_PER_MILLI,
timestamp: event.time * MICROS_PER_MILLI,
value: event.name,
}));
}
7 changes: 6 additions & 1 deletion packages/opentelemetry-exporter-zipkin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export interface ExporterConfig {
logger?: types.Logger;
serviceName: string;
url?: string;
// Initiates a request with spans in memory to the backend.
forceFlush?: boolean;
// Optional mapping overrides for OpenTelemetry status code and description.
statusCodeTagName?: string;
statusDescriptionTagName?: string;
}

/**
Expand Down Expand Up @@ -51,7 +56,7 @@ export interface Span {
* When present, kind clarifies timestamp, duration and remoteEndpoint.
* When absent, the span is local or incomplete.
*/
kind: SpanKind;
kind?: SpanKind;
/**
* Epoch microseconds of the start of this span, possibly absent if
* incomplete.
Expand Down
61 changes: 49 additions & 12 deletions packages/opentelemetry-exporter-zipkin/src/zipkin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,35 @@ import {
} from '@opentelemetry/basic-tracer';

import * as zipkinTypes from './types';
import { toZipkinSpan } from './transform';
import {
toZipkinSpan,
STATUS_CODE_TAG_NAME,
STATUS_DESCRIPTION_TAG_NAME,
} from './transform';

/**
* Zipkin Exporter
*/
export class ZipkinExporter implements SpanExporter {
static readonly DEFAULT_URL = 'http://localhost:9411/api/v2/spans';

private readonly _url: url.UrlWithStringQuery;
private readonly _serviceName: string;
private readonly _forceFlush: boolean;
private readonly _logger: types.Logger;
private readonly _serviceName: string;
private readonly _statusCodeTagName: string;
private readonly _statusDescriptionTagName: string;
private readonly _url: url.UrlWithStringQuery;

constructor(config: zipkinTypes.ExporterConfig) {
const urlStr = config.url || ZipkinExporter.DEFAULT_URL;

this._url = url.parse(urlStr);
this._serviceName = config.serviceName;
this._forceFlush = config.forceFlush || true;
this._logger = config.logger || new NoopLogger();
this._serviceName = config.serviceName;
this._statusCodeTagName = config.statusCodeTagName || STATUS_CODE_TAG_NAME;
this._statusDescriptionTagName =
config.statusDescriptionTagName || STATUS_DESCRIPTION_TAG_NAME;
this._url = url.parse(urlStr);
}

/**
Expand All @@ -54,26 +65,47 @@ export class ZipkinExporter implements SpanExporter {
resultCallback: (result: ExportResult) => void
) {
this._logger.debug('Zipkin exporter export');
// TODO: buffer spans (batch based on both time and max number)
const zipkinSpans = spans.map(this._toZipkinSpan);
// TODO: will the caller retry and manage backoff in the case ExportResult
// is FailedRetriable?
return this._send(zipkinSpans, resultCallback);
// @todo: buffer spans (batch based on both time and max number)
return this._sendSpans(spans, resultCallback);
}

/**
* Shutdown exporter. Noop operation in this exporter.
*/
shutdown() {
// TODO: should we initiate an oppurtinistic send(..)?
this._logger.debug('Zipkin exporter shutdown');
// Make an optimistic flush.
if (this._forceFlush) {
// @todo get spans from span processor (batch)
this._sendSpans([]);
}
}

/**
* Transforms an OpenTelemetry span to a Zipkin span.
*/
private _toZipkinSpan(span: ReadableSpan): zipkinTypes.Span {
return toZipkinSpan(this._serviceName, span);
return toZipkinSpan(
span,
this._serviceName,
this._statusCodeTagName,
this._statusDescriptionTagName
);
}

/**
* Transform spans and sends to Zipkin service.
*/
private _sendSpans(
spans: ReadableSpan[],
done?: (result: ExportResult) => void
) {
const zipkinSpans = spans.map((span) => this._toZipkinSpan(span));
return this._send(zipkinSpans, (result: ExportResult) => {
if (done) {
return done(result);
}
});
}

/**
Expand All @@ -83,6 +115,11 @@ export class ZipkinExporter implements SpanExporter {
zipkinSpans: zipkinTypes.Span[],
done: (result: ExportResult) => void
) {
if (zipkinSpans.length === 0) {
this._logger.debug('Zipkin send with empty spans');
return done(ExportResult.Success);
}

const options = {
hostname: this._url.hostname,
port: this._url.port,
Expand Down
64 changes: 60 additions & 4 deletions packages/opentelemetry-exporter-zipkin/test/zipkin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,71 @@
* limitations under the License.
*/

import * as assert from 'assert';
import * as nock from 'nock';
import {
BasicTracer,
ExportResult,
ReadableSpan,
} from '@opentelemetry/basic-tracer';
import { NoopScopeManager } from '@opentelemetry/scope-base';
import { NoopLogger } from '@opentelemetry/core';
import { ZipkinExporter } from '../src';
import * as zipkinTypes from '../src/types';

describe('ZipkinExporter', () => {
describe('constructor', () => {
it('should construct an exporter');
it('should construct an exporter with url');
it('should construct an exporter with logger');
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');
});
});

describe('export', () => {
it('should send spans to Zipkin backend');
it('should send spans to Zipkin backend', () => {
let requestBody: [zipkinTypes.Span];
const scope = nock('http://localhost:9411')
.post('/api/v2/spans', (body: [zipkinTypes.Span]) => {
requestBody = body;
return true;
})
.reply(202);

const tracer = new BasicTracer({
scopeManager: new NoopScopeManager(),
});
const span1 = tracer.startSpan({ name: 'my-span' });
span1.end();
const spans = [span1];

const exporter = new ZipkinExporter({
serviceName: 'my-service',
logger: new NoopLogger(),
});

exporter.export(spans as [ReadableSpan], (result: ExportResult) => {
scope.done();
assert.strictEqual(result, ExportResult.Success);
assert.deepStrictEqual(requestBody, []);
});
});
});

describe('shutdown', () => {
Expand Down

0 comments on commit 945cfe1

Please sign in to comment.