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: adding possibility of recording exception #1372

Merged
merged 16 commits into from
Aug 24, 2020
Merged
22 changes: 22 additions & 0 deletions packages/opentelemetry-api/src/common/Exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.
*/

/**
* Defines Exception.
*
* Error or string
*/
export type Exception = Partial<Error> | string;
obecny marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions packages/opentelemetry-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

export * from './common/Exception';
export * from './common/Logger';
export * from './common/Time';
export * from './context/propagation/getter';
Expand Down
4 changes: 4 additions & 0 deletions packages/opentelemetry-api/src/trace/NoopSpan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import { Exception } from '../common/Exception';
import { TimeInput } from '../common/Time';
import { Attributes } from './attributes';
import { Span } from './span';
Expand Down Expand Up @@ -76,6 +77,9 @@ export class NoopSpan implements Span {
isRecording(): boolean {
return false;
}

// By default does nothing
recordException(exception: Exception, time?: TimeInput): void {}
}

export const NOOP_SPAN = new NoopSpan();
9 changes: 9 additions & 0 deletions packages/opentelemetry-api/src/trace/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import { Exception } from '../common/Exception';
import { Attributes } from './attributes';
import { SpanContext } from './span_context';
import { Status } from './status';
Expand Down Expand Up @@ -114,4 +115,12 @@ export interface Span {
* with the `AddEvent` operation and attributes using `setAttributes`.
*/
isRecording(): boolean;

/**
* Sets exception as a span event
* @param exception the exception the only accepted values are string or Error
* @param [time] the time to set as Span's event time. If not provided,
* use the current time.
*/
recordException(exception: Exception, time?: TimeInput): void;
obecny marked this conversation as resolved.
Show resolved Hide resolved
}
3 changes: 2 additions & 1 deletion packages/opentelemetry-plugin-http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"precompile": "tsc --version",
"version:update": "node ../../scripts/version-update.js",
"compile": "npm run version:update && tsc -p .",
"prepare": "npm run compile"
"prepare": "npm run compile",
"watch": "tsc -w"
},
"keywords": [
"opentelemetry",
Expand Down
3 changes: 2 additions & 1 deletion packages/opentelemetry-semantic-conventions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"precompile": "tsc --version",
"version:update": "node ../../scripts/version-update.js",
"compile": "npm run version:update && tsc -p .",
"prepare": "npm run compile"
"prepare": "npm run compile",
"watch": "tsc -w"
},
"keywords": [
"opentelemetry",
Expand Down
23 changes: 23 additions & 0 deletions packages/opentelemetry-semantic-conventions/src/trace/exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.
*/

export const ExceptionAttribute = {
MESSAGE: 'exception.message',
STACKTRACE: 'exception.stacktrace',
TYPE: 'exception.type',
};

export const ExceptionEventName = 'exception';
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
* limitations under the License.
*/

export * from './database';
export * from './exception';
export * from './general';
export * from './rpc';
export * from './http';
export * from './database';
export * from './rpc';
3 changes: 2 additions & 1 deletion packages/opentelemetry-tracing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"@opentelemetry/api": "^0.10.1",
"@opentelemetry/context-base": "^0.10.1",
"@opentelemetry/core": "^0.10.1",
"@opentelemetry/resources": "^0.10.1"
"@opentelemetry/resources": "^0.10.1",
"@opentelemetry/semantic-conventions": "^0.10.1"
}
}
34 changes: 34 additions & 0 deletions packages/opentelemetry-tracing/src/Span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
timeInputToHrTime,
} from '@opentelemetry/core';
import { Resource } from '@opentelemetry/resources';
import {
ExceptionAttribute,
ExceptionEventName,
} from '@opentelemetry/semantic-conventions';
import { ReadableSpan } from './export/ReadableSpan';
import { Tracer } from './Tracer';
import { SpanProcessor } from './SpanProcessor';
Expand Down Expand Up @@ -178,6 +182,36 @@ export class Span implements api.Span, ReadableSpan {
return true;
}

recordException(exception: api.Exception, time: api.TimeInput = hrTime()) {
const attributes: api.Attributes = {};
if (!exception) {
dyladan marked this conversation as resolved.
Show resolved Hide resolved
return;
}
if (typeof exception === 'string') {
attributes[ExceptionAttribute.MESSAGE] = exception;
} else {
if (exception.name) {
attributes[ExceptionAttribute.TYPE] = exception.name;
}
if (exception.message) {
attributes[ExceptionAttribute.MESSAGE] = exception.message;
}
if (exception.stack) {
attributes[ExceptionAttribute.STACKTRACE] = exception.stack;
}
}

// these are minimum requirements from spec
if (
attributes[ExceptionAttribute.TYPE] ||
attributes[ExceptionAttribute.MESSAGE]
) {
this.addEvent(ExceptionEventName, attributes as api.Attributes, time);
} else {
this._logger.error(`Failed to record an exception ${exception}`);
}
obecny marked this conversation as resolved.
Show resolved Hide resolved
}

get duration(): api.HrTime {
return this._duration;
}
Expand Down
84 changes: 84 additions & 0 deletions packages/opentelemetry-tracing/test/Span.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
* limitations under the License.
*/

import { ExceptionAttribute } from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import {
SpanKind,
CanonicalCode,
TraceFlags,
SpanContext,
LinkContext,
Exception,
} from '@opentelemetry/api';
import { BasicTracerProvider, Span } from '../src';
import {
Expand Down Expand Up @@ -357,4 +359,86 @@ describe('Span', () => {
span.end();
assert.strictEqual(span.ended, true);
});

describe('recordException', () => {
const invalidExceptions: any[] = [
1,
null,
undefined,
{ foo: 'bar' },
{ stack: 'bar' },
['a', 'b', 'c'],
];

invalidExceptions.forEach(key => {
describe(`when exception is (${JSON.stringify(key)})`, () => {
it('should NOT record an exception', () => {
const span = new Span(tracer, name, spanContext, SpanKind.CLIENT);
assert.strictEqual(span.events.length, 0);
span.recordException(key);
assert.strictEqual(span.events.length, 0);
});
});
});

describe('when exception type is "string"', () => {
let error: Exception;
beforeEach(() => {
error = 'boom';
});
it('should record an exception', () => {
const span = new Span(tracer, name, spanContext, SpanKind.CLIENT);
assert.strictEqual(span.events.length, 0);
span.recordException(error);

const event = span.events[0];
assert.strictEqual(event.name, 'exception');
assert.deepStrictEqual(event.attributes, {
'exception.message': 'boom',
});
assert.ok(event.time[0] > 0);
});
});

describe('when exception type is "Error"', () => {
let error: Exception;
beforeEach(() => {
try {
throw new Error('boom');
} catch (e) {
error = e;
}
});
it('should record an exception', () => {
const span = new Span(tracer, name, spanContext, SpanKind.CLIENT);
assert.strictEqual(span.events.length, 0);
span.recordException(error);

const event = span.events[0];
assert.ok(event.time[0] > 0);
assert.strictEqual(event.name, 'exception');

assert.ok(event.attributes);

const type = event.attributes[ExceptionAttribute.TYPE];
const message = event.attributes[ExceptionAttribute.MESSAGE];
const stacktrace = String(
event.attributes[ExceptionAttribute.STACKTRACE]
);
assert.strictEqual(type, 'Error');
assert.strictEqual(message, 'boom');
assert.ok(stacktrace.indexOf('Error: boom') >= 0);
});
});

describe('when time is provided', () => {
it('should record an exception with provided time', () => {
const span = new Span(tracer, name, spanContext, SpanKind.CLIENT);
assert.strictEqual(span.events.length, 0);
span.recordException('boom', [0, 123]);
const event = span.events[0];
assert.deepStrictEqual(event.time, [0, 123]);
});
});
});
});