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(opentelemetry-js): infer zipkin service name from resource #1138

Merged
merged 9 commits into from
Jun 6, 2020
2 changes: 1 addition & 1 deletion packages/opentelemetry-exporter-zipkin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ npm install --save @opentelemetry/exporter-zipkin

## Usage

Install the exporter on your application and pass the options, it must contain a service name.
Install the exporter on your application and pass the options. `serviceName` is an optional string. If omitted, the exporter will first try to get the service name from the Resource. If no service name can be detected on the Resource, a fallback name of "OpenTelemetry Service" will be used.

```js
const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin');
Expand Down
2 changes: 1 addition & 1 deletion packages/opentelemetry-exporter-zipkin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import * as api from '@opentelemetry/api';
*/
export interface ExporterConfig {
logger?: api.Logger;
serviceName: string;
serviceName?: string;
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice if you can update the https://github.com/rezakrimi/opentelemetry-js/tree/zipkin-service-name/packages/opentelemetry-exporter-zipkin#usage and mention "serviceName is optional and can be omitted - it will be inferred from resource and in case not provided, serviceName will default to "OpenTelemetry Service"". Something like this.

Copy link
Contributor Author

@rezakrimi rezakrimi Jun 4, 2020

Choose a reason for hiding this comment

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

How about we mention that it's completely optional to pass the config now. So the usage section will be something like:
"Install the exporter on your application and pass the options. It's optional to pass the options. If serviceName is omitted, it will be inferred from resource and in case not provided, serviceName will default to "OpenTelemetry Service". The url is also set to http://localhost:9411/api/v2/spans by default."

Copy link
Member

Choose a reason for hiding this comment

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

It's not exactly obvious how to set the service name in the resource right now, so I would focus more on the case where you do directly set the service name:

serviceName is an optional string. If omitted, the exporter will first try to get the service name from the Resource. If no service name can be detected on the Resource, a fallback name of "OpenTelemetry Service" will be used.

url?: string;
// Optional mapping overrides for OpenTelemetry status code and description.
statusCodeTagName?: string;
Expand Down
36 changes: 20 additions & 16 deletions packages/opentelemetry-exporter-zipkin/src/zipkin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,21 @@ import {
statusDescriptionTagName,
} from './transform';
import { OT_REQUEST_HEADER } from './utils';
import { SERVICE_RESOURCE } from '@opentelemetry/resources';
/**
* Zipkin Exporter
*/
export class ZipkinExporter implements SpanExporter {
static readonly DEFAULT_URL = 'http://localhost:9411/api/v2/spans';
private readonly DEFAULT_SERVICE_NAME = 'OpenTelemetry Service';
private readonly _logger: api.Logger;
private readonly _serviceName: string;
private readonly _statusCodeTagName: string;
private readonly _statusDescriptionTagName: string;
private readonly _reqOpts: http.RequestOptions;
private _serviceName?: string;
private _isShutdown: boolean;

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

Expand Down Expand Up @@ -68,12 +70,18 @@ export class ZipkinExporter implements SpanExporter {
spans: ReadableSpan[],
resultCallback: (result: ExportResult) => void
) {
if (typeof this._serviceName !== 'string') {
dyladan marked this conversation as resolved.
Show resolved Hide resolved
this._serviceName = String(
spans[0].resource.labels[SERVICE_RESOURCE.NAME] ||
this.DEFAULT_SERVICE_NAME
);
}
this._logger.debug('Zipkin exporter export');
if (this._isShutdown) {
setTimeout(() => resultCallback(ExportResult.FAILED_NOT_RETRYABLE));
return;
}
return this._sendSpans(spans, resultCallback);
return this._sendSpans(spans, this._serviceName, resultCallback);
}

/**
Expand All @@ -87,26 +95,22 @@ export class ZipkinExporter implements SpanExporter {
this._isShutdown = true;
}

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

/**
* Transform spans and sends to Zipkin service.
*/
private _sendSpans(
spans: ReadableSpan[],
serviceName: string,
done?: (result: ExportResult) => void
) {
const zipkinSpans = spans.map(span => this._toZipkinSpan(span));
const zipkinSpans = spans.map(span =>
toZipkinSpan(
span,
serviceName,
this._statusCodeTagName,
this._statusDescriptionTagName
)
);
return this._send(zipkinSpans, (result: ExportResult) => {
if (done) {
return done(result);
Expand Down
147 changes: 147 additions & 0 deletions packages/opentelemetry-exporter-zipkin/test/zipkin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ZipkinExporter } from '../src';
import * as zipkinTypes from '../src/types';
import { OT_REQUEST_HEADER } from '../src/utils';
import { TraceFlags } from '@opentelemetry/api';
import { SERVICE_RESOURCE } from '@opentelemetry/resources';

const MICROS_PER_SECS = 1e6;

Expand Down Expand Up @@ -331,6 +332,152 @@ describe('ZipkinExporter', () => {
done();
});
});

it('should set serviceName to "Opentelemetry Service" by default', () => {
const scope = nock('http://localhost:9411')
.post('/api/v2/spans')
.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.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: 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.CanonicalCode.OK,
},
attributes: {},
links: [],
events: [],
resource: Resource.empty(),
};

const exporter = new ZipkinExporter({});

exporter.export([span1, span2], (result: ExportResult) => {
scope.done();
assert.equal(exporter['_serviceName'], 'OpenTelemetry Service');
});
});

it('should set serviceName if resource has one', () => {
const resource_service_name = 'resource_service_name';

const scope = nock('http://localhost:9411')
.post('/api/v2/spans')
.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.CanonicalCode.OK,
},
attributes: {
key1: 'value1',
key2: 'value2',
},
links: [],
events: [
{
name: 'my-event',
time: [startTime + 10, 0],
attributes: { key3: 'value3' },
},
],
resource: new Resource({
[SERVICE_RESOURCE.NAME]: resource_service_name,
}),
};
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.CanonicalCode.OK,
},
attributes: {},
links: [],
events: [],
resource: Resource.empty(),
};

const exporter = new ZipkinExporter({});

exporter.export([span1, span2], (result: ExportResult) => {
scope.done();
assert.equal(exporter['_serviceName'], resource_service_name);

// checking if service name remains consistent in further exports
exporter.export([span2], (result: ExportResult) => {
scope.done();
assert.equal(exporter['_serviceName'], resource_service_name);
});
});
});
});

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