Skip to content

Commit

Permalink
feat: add Jaeger exporter (#231)
Browse files Browse the repository at this point in the history
* feat: add jaeger exporter

* feat: add transform tests

* feat: add jaege tests

* fix: transform span.status to tags

* fix: use NoopLogger

* fix: use named constant

* fix: transform event->attributes to fields

* fix: transform links to ThriftReference

* fix: make _sender readonly

* fix: add status.name

* fix: add @todos
  • Loading branch information
mayurkale22 committed Sep 9, 2019
1 parent c8a37be commit c020efe
Show file tree
Hide file tree
Showing 8 changed files with 662 additions and 1 deletion.
46 changes: 45 additions & 1 deletion packages/opentelemetry-exporter-jaeger/README.md
Expand Up @@ -14,14 +14,58 @@ OpenTelemetry Jaeger Trace Exporter allows the user to send collected traces to
- Service dependency analysis
- Performance / latency optimization

## Prerequisites

Get up and running with Jaeger in your local environment.

[Jaeger](https://www.jaegertracing.io/docs/1.13/) stores and queries traces exported by
applications instrumented with OpenTelemetry. The easiest way to [start a Jaeger
server](https://www.jaegertracing.io/docs/1.13/getting-started/) is to paste the below:

```bash
docker run -d --name jaeger \
-e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
-p 5775:5775/udp \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14268:14268 \
-p 9411:9411 \
jaegertracing/all-in-one:1.13
```

Or run the `jaeger-all-in-one(.exe)` executable from the [binary distribution archives](https://www.jaegertracing.io/download/):

```bash
jaeger-all-in-one --collector.zipkin.http-port=9411
```

You can then navigate to http://localhost:16686 to access the Jaeger UI.

## Installation

```
npm install --save @opentelemetry/exporter-jaeger
```

## Usage
> TODO

Install the exporter on your application and pass the options, it must contain a service name.

```
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
const options = {
serviceName: 'my-service',
tags: [], // optional
host: 'localhost', // optional
port: 6832, // optional
maxPacketSize: 65000 // optional
}
const exporter = new JaegerExporter(options);
```
> TODO : add how to register the exporter.
## Useful links
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
Expand Down
5 changes: 5 additions & 0 deletions packages/opentelemetry-exporter-jaeger/package.json
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'",
"tdd": "yarn test -- --watch-extensions ts --watch",
"codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../",
"clean": "rimraf build/*",
"check": "gts check",
"compile": "tsc -p .",
Expand Down Expand Up @@ -46,5 +47,9 @@
"typescript": "^3.5.3"
},
"dependencies": {
"@opentelemetry/core": "^0.0.1",
"@opentelemetry/basic-tracer": "^0.0.1",
"@opentelemetry/types": "^0.0.1",
"jaeger-client": "^3.15.0"
}
}
2 changes: 2 additions & 0 deletions packages/opentelemetry-exporter-jaeger/src/index.ts
Expand Up @@ -13,3 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export * from './jaeger';
81 changes: 81 additions & 0 deletions packages/opentelemetry-exporter-jaeger/src/jaeger.ts
@@ -0,0 +1,81 @@
/**
* 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 {
SpanExporter,
ReadableSpan,
ExportResult,
} from '@opentelemetry/basic-tracer';
import * as jaegerTypes from './types';
import { NoopLogger } from '@opentelemetry/core';
import * as types from '@opentelemetry/types';
import { spanToThrift } from './transform';

/**
* Format and sends span information to Jaeger Exporter.
*/
export class JaegerExporter implements SpanExporter {
private readonly _logger: types.Logger;
private readonly _process: jaegerTypes.ThriftProcess;
private readonly _sender: typeof jaegerTypes.UDPSender;

constructor(config: jaegerTypes.ExporterConfig) {
this._logger = config.logger || new NoopLogger();
const tags: jaegerTypes.Tag[] = config.tags || [];

this._sender = new jaegerTypes.UDPSender(config);
this._process = {
serviceName: config.serviceName,
tags: jaegerTypes.ThriftUtils.getThriftTags(tags),
};
this._sender.setProcess(this._process);
}

/** Exports a list of spans to Jaeger. */
export(
spans: ReadableSpan[],
resultCallback: (result: ExportResult) => void
): void {
this._logger.debug('Jaeger exporter export');
return this._sendSpans(spans, resultCallback);
}

/** Shutdown exporter. */
shutdown(): void {
this._sender.close();
}

/** Transform spans and sends to Jaeger service. */
private _sendSpans(
spans: ReadableSpan[],
done?: (result: ExportResult) => void
) {
const thriftSpan = spans.map(span => spanToThrift(span));
for (const span of thriftSpan) {
this._sender.append(span, (numSpans: number, err?: string) => {
if (err) {
// @todo: decide whether to break out the loop on first error.
this._logger.error(`failed to append span: ${err}`);
if (done) return done(ExportResult.FailedNonRetryable);
}
});
}
// @todo: We should wait for all the callbacks of the append calls to
// complete before it calls done with success.
this._logger.debug('successful append for : %s', thriftSpan.length);
if (done) return done(ExportResult.Success);
}
}
121 changes: 121 additions & 0 deletions packages/opentelemetry-exporter-jaeger/src/transform.ts
@@ -0,0 +1,121 @@
/**
* 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 { Link, CanonicalCode } from '@opentelemetry/types';
import { ReadableSpan } from '@opentelemetry/basic-tracer';
import {
ThriftSpan,
Tag,
Log,
ThriftTag,
ThriftLog,
ThriftUtils,
Utils,
ThriftReference,
TagValue,
ThriftReferenceType,
} from './types';

const MICROS_PER_MILLI = 1000;
const DEFAULT_FLAGS = 0x1;

/**
* Translate OpenTelemetry ReadableSpan to Jaeger Thrift Span
* @param span Span to be translated
*/
export function spanToThrift(span: ReadableSpan): ThriftSpan {
const traceIdHigh = span.spanContext.traceId.slice(0, 16);
const traceIdLow = span.spanContext.traceId.slice(16);
const parentSpan = span.parentSpanId
? Utils.encodeInt64(span.parentSpanId)
: ThriftUtils.emptyBuffer;

const tags = Object.keys(span.attributes).map(
(name): Tag => ({ key: name, value: toTagValue(span.attributes[name]) })
);
tags.push({ key: 'status.code', value: span.status.code });
tags.push({ key: 'status.name', value: CanonicalCode[span.status.code] });
if (span.status.message) {
tags.push({ key: 'status.message', value: span.status.message });
}
// Ensure that if Status.Code is not OK, that we set the "error" tag on the
// Jaeger span.
if (span.status.code !== CanonicalCode.OK) {
tags.push({ key: 'error', value: true });
}
const spanTags: ThriftTag[] = ThriftUtils.getThriftTags(tags);

const logs = span.events.map(
(event): Log => {
const fields: Tag[] = [{ key: 'message.id', value: event.name }];
const attrs = event.attributes;
if (attrs) {
Object.keys(attrs).forEach(attr =>
fields.push({ key: attr, value: toTagValue(attrs[attr]) })
);
}
return { timestamp: event.time, fields };
}
);
const spanLogs: ThriftLog[] = ThriftUtils.getThriftLogs(logs);

return {
traceIdLow: Utils.encodeInt64(traceIdLow),
traceIdHigh: Utils.encodeInt64(traceIdHigh),
spanId: Utils.encodeInt64(span.spanContext.spanId),
parentSpanId: parentSpan,
operationName: span.name,
references: spanLinksToThriftRefs(span.links, span.parentSpanId),
flags: span.spanContext.traceOptions || DEFAULT_FLAGS,
startTime: Utils.encodeInt64(span.startTime * MICROS_PER_MILLI),
duration: Utils.encodeInt64(
Math.round((span.endTime - span.startTime) * MICROS_PER_MILLI)
),
tags: spanTags,
logs: spanLogs,
};
}

/** Translate OpenTelemetry {@link Link}s to Jaeger ThriftReference. */
function spanLinksToThriftRefs(
links: Link[],
parentSpanId?: string
): ThriftReference[] {
return links
.map((link): ThriftReference | null => {
if (link.spanContext.spanId === parentSpanId) {
const refType = ThriftReferenceType.CHILD_OF;
const traceId = link.spanContext.traceId;
const traceIdHigh = Utils.encodeInt64(traceId.slice(0, 16));
const traceIdLow = Utils.encodeInt64(traceId.slice(16));
const spanId = Utils.encodeInt64(link.spanContext.spanId);
return { traceIdLow, traceIdHigh, spanId, refType };
}
return null;
})
.filter(ref => !!ref) as ThriftReference[];
}

/** Translate OpenTelemetry attribute value to Jaeger TagValue. */
function toTagValue(value: unknown): TagValue {
const valueType = typeof value;
if (valueType === 'boolean') {
return value as boolean;
} else if (valueType === 'number') {
return value as number;
}
return String(value);
}
98 changes: 98 additions & 0 deletions packages/opentelemetry-exporter-jaeger/src/types.ts
@@ -0,0 +1,98 @@
/**
* 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 types from '@opentelemetry/types';

/**
* Options for Jaeger configuration
*/
export interface ExporterConfig {
logger?: types.Logger;
serviceName: string;
tags?: Tag[];
host?: string; // default: 'localhost'
port?: number; // default: 6832
maxPacketSize?: number; // default: 65000
}

// Below require is needed as jaeger-client types does not expose the thrift,
// udp_sender, util etc. modules.

// tslint:disable-next-line:variable-name
export const UDPSender = require('jaeger-client/dist/src/reporters/udp_sender')
.default;
// tslint:disable-next-line:variable-name
export const Utils = require('jaeger-client/dist/src/util').default;
// tslint:disable-next-line:variable-name
export const ThriftUtils = require('jaeger-client/dist/src/thrift').default;

export type TagValue = string | number | boolean;

export interface Tag {
key: string;
value: TagValue;
}

export interface Log {
timestamp: number;
fields: Tag[];
}

export type SenderCallback = (numSpans: number, err?: string) => void;

export interface ThriftProcess {
serviceName: string;
tags: ThriftTag[];
}

export interface ThriftTag {
key: string;
vType: string;
vStr: string;
vDouble: number;
vBool: boolean;
}

export interface ThriftLog {
timestamp: number;
fields: ThriftTag[];
}

export enum ThriftReferenceType {
CHILD_OF = 'CHILD_OF',
FOLLOWS_FROM = 'FOLLOWS_FROM',
}

export interface ThriftReference {
traceIdLow: Buffer;
traceIdHigh: Buffer;
spanId: Buffer;
refType: ThriftReferenceType;
}

export interface ThriftSpan {
traceIdLow: Buffer;
traceIdHigh: Buffer;
spanId: Buffer;
parentSpanId: string | Buffer;
operationName: string;
references: ThriftReference[];
flags: number;
startTime: number; // milliseconds
duration: number; // milliseconds
tags: ThriftTag[];
logs: ThriftLog[];
}

0 comments on commit c020efe

Please sign in to comment.