Skip to content

Commit 240f852

Browse files
authored
feat: add global error handler (#1514)
1 parent 60d4dab commit 240f852

File tree

29 files changed

+582
-155
lines changed

29 files changed

+582
-155
lines changed

packages/opentelemetry-api/src/common/Exception.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,21 @@
1515
*/
1616

1717
interface ExceptionWithCode {
18-
code: string;
18+
code: string | number;
1919
name?: string;
2020
message?: string;
2121
stack?: string;
2222
}
2323

2424
interface ExceptionWithMessage {
25-
code?: string;
25+
code?: string | number;
2626
message: string;
2727
name?: string;
2828
stack?: string;
2929
}
3030

3131
interface ExceptionWithName {
32-
code?: string;
32+
code?: string | number;
3333
message?: string;
3434
name: string;
3535
stack?: string;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Exception } from '@opentelemetry/api';
18+
import { loggingErrorHandler } from './logging-error-handler';
19+
import { ErrorHandler } from './types';
20+
21+
/** The global error handler delegate */
22+
let delegateHandler = loggingErrorHandler();
23+
24+
/**
25+
* Set the global error handler
26+
* @param {ErrorHandler} handler
27+
*/
28+
export function setGlobalErrorHandler(handler: ErrorHandler) {
29+
delegateHandler = handler;
30+
}
31+
32+
/**
33+
* Return the global error handler
34+
* @param {Exception} ex
35+
*/
36+
export const globalErrorHandler = (ex: Exception) => {
37+
try {
38+
delegateHandler(ex);
39+
} catch {} // eslint-disable-line no-empty
40+
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Logger, Exception } from '@opentelemetry/api';
18+
import { ConsoleLogger } from './ConsoleLogger';
19+
import { ErrorHandler, LogLevel } from './types';
20+
21+
/**
22+
* Returns a function that logs an error using the provided logger, or a
23+
* console logger if one was not provided.
24+
* @param {Logger} logger
25+
*/
26+
export function loggingErrorHandler(logger?: Logger): ErrorHandler {
27+
logger = logger ?? new ConsoleLogger(LogLevel.ERROR);
28+
return (ex: Exception) => {
29+
logger!.error(stringifyException(ex));
30+
};
31+
}
32+
33+
/**
34+
* Converts an exception into a string representation
35+
* @param {Exception} ex
36+
*/
37+
function stringifyException(ex: Exception | string): string {
38+
if (typeof ex === 'string') {
39+
return ex;
40+
} else {
41+
return JSON.stringify(flattenException(ex));
42+
}
43+
}
44+
45+
/**
46+
* Flattens an exception into key-value pairs by traversing the prototype chain
47+
* and coercing values to strings. Duplicate properties will not be overwritten;
48+
* the first insert wins.
49+
*/
50+
function flattenException(ex: Exception): Record<string, string> {
51+
const result = {} as Record<string, string>;
52+
let current = ex;
53+
54+
while (current !== null) {
55+
Object.getOwnPropertyNames(current).forEach(propertyName => {
56+
if (result[propertyName]) return;
57+
const value = current[propertyName as keyof typeof current];
58+
if (value) {
59+
result[propertyName] = String(value);
60+
}
61+
});
62+
current = Object.getPrototypeOf(current);
63+
}
64+
65+
return result;
66+
}

packages/opentelemetry-core/src/common/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
17+
import { Exception } from '@opentelemetry/api';
18+
1619
export enum LogLevel {
1720
ERROR,
1821
WARN,
@@ -56,3 +59,6 @@ export interface InstrumentationLibrary {
5659
readonly name: string;
5760
readonly version: string;
5861
}
62+
63+
/** Defines an error handler function */
64+
export type ErrorHandler = (ex: Exception) => void;

packages/opentelemetry-core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
export * from './common/attributes';
1818
export * from './common/ConsoleLogger';
19+
export * from './common/global-error-handler';
20+
export * from './common/logging-error-handler';
1921
export * from './common/NoopLogger';
2022
export * from './common/time';
2123
export * from './common/types';
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert';
18+
import * as sinon from 'sinon';
19+
import { globalErrorHandler, setGlobalErrorHandler } from '../../src';
20+
import { Exception } from '@opentelemetry/api';
21+
22+
describe('globalErrorHandler', () => {
23+
let defaultHandler: sinon.SinonSpy;
24+
25+
beforeEach(() => {
26+
defaultHandler = sinon.spy();
27+
setGlobalErrorHandler(defaultHandler);
28+
});
29+
30+
it('receives errors', () => {
31+
const err = new Error('this is bad');
32+
globalErrorHandler(err);
33+
sinon.assert.calledOnceWithExactly(defaultHandler, err);
34+
});
35+
36+
it('replaces delegate when handler is updated', () => {
37+
const err = new Error('this is bad');
38+
const newHandler = sinon.spy();
39+
setGlobalErrorHandler(newHandler);
40+
41+
globalErrorHandler(err);
42+
43+
sinon.assert.calledOnceWithExactly(newHandler, err);
44+
sinon.assert.notCalled(defaultHandler);
45+
});
46+
47+
it('catches exceptions thrown in handler', () => {
48+
setGlobalErrorHandler((ex: Exception) => {
49+
throw new Error('bad things');
50+
});
51+
52+
assert.doesNotThrow(() => {
53+
globalErrorHandler('an error');
54+
});
55+
});
56+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert';
18+
import * as sinon from 'sinon';
19+
import { ErrorHandler, loggingErrorHandler } from '../../src';
20+
21+
describe('loggingErrorHandler', () => {
22+
let handler: ErrorHandler;
23+
const errorStub = sinon.fake();
24+
25+
beforeEach(() => {
26+
handler = loggingErrorHandler({
27+
debug: sinon.fake(),
28+
info: sinon.fake(),
29+
warn: sinon.fake(),
30+
error: errorStub,
31+
});
32+
});
33+
34+
it('logs from string', () => {
35+
const err = 'not found';
36+
handler(err);
37+
assert.ok(errorStub.calledOnceWith(err));
38+
});
39+
40+
it('logs from an object', () => {
41+
const err = {
42+
name: 'NotFoundError',
43+
message: 'not found',
44+
randomString: 'random value',
45+
randomNumber: 42,
46+
randomArray: [1, 2, 3],
47+
randomObject: { a: 'a' },
48+
stack: 'a stack',
49+
};
50+
51+
handler(err);
52+
53+
const [result] = errorStub.lastCall.args;
54+
55+
assert.ok(result.includes(err.name));
56+
assert.ok(result.includes(err.message));
57+
assert.ok(result.includes(err.randomString));
58+
assert.ok(result.includes(err.randomNumber));
59+
assert.ok(result.includes(err.randomArray));
60+
assert.ok(result.includes(err.randomObject));
61+
assert.ok(result.includes(JSON.stringify(err.stack)));
62+
});
63+
64+
it('logs from an error', () => {
65+
const err = new Error('this is bad');
66+
67+
handler(err);
68+
69+
const [result] = errorStub.lastCall.args;
70+
71+
assert.ok(result.includes(err.name));
72+
assert.ok(result.includes(err.message));
73+
assert.ok(result.includes(JSON.stringify(err.stack)));
74+
});
75+
});

packages/opentelemetry-exporter-collector-proto/src/util.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,6 @@ export function send<ExportItem, ServiceRequest>(
8080
);
8181
}
8282
} else {
83-
onError({
84-
message: 'No proto',
85-
});
83+
onError(new collectorTypes.CollectorExporterError('No proto'));
8684
}
8785
}

packages/opentelemetry-exporter-collector-proto/test/CollectorMetricExporter.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,14 @@ describe('CollectorMetricExporter - node with proto over http', () => {
177177
});
178178

179179
it('should log the error message', done => {
180-
const spyLoggerError = sinon.stub(collectorExporter.logger, 'error');
180+
const spyLoggerError = sinon.spy();
181+
const handler = core.loggingErrorHandler({
182+
debug: sinon.fake(),
183+
info: sinon.fake(),
184+
warn: sinon.fake(),
185+
error: spyLoggerError,
186+
});
187+
core.setGlobalErrorHandler(handler);
181188

182189
const responseSpy = sinon.spy();
183190
collectorExporter.export(metrics, responseSpy);
@@ -187,9 +194,8 @@ describe('CollectorMetricExporter - node with proto over http', () => {
187194
const callback = args[1];
188195
callback(mockResError);
189196
setTimeout(() => {
190-
const response: any = spyLoggerError.args[0][0];
191-
assert.strictEqual(response, 'statusCode: 400');
192-
197+
const response = spyLoggerError.args[0][0] as string;
198+
assert.ok(response.includes('"code":"400"'));
193199
assert.strictEqual(responseSpy.args[0][0], 1);
194200
done();
195201
});

packages/opentelemetry-exporter-collector-proto/test/CollectorTraceExporter.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,14 @@ describe('CollectorTraceExporter - node with proto over http', () => {
144144
});
145145

146146
it('should log the error message', done => {
147-
const spyLoggerError = sinon.stub(collectorExporter.logger, 'error');
147+
const spyLoggerError = sinon.spy();
148+
const handler = core.loggingErrorHandler({
149+
debug: sinon.fake(),
150+
info: sinon.fake(),
151+
warn: sinon.fake(),
152+
error: spyLoggerError,
153+
});
154+
core.setGlobalErrorHandler(handler);
148155

149156
const responseSpy = sinon.spy();
150157
collectorExporter.export(spans, responseSpy);
@@ -154,9 +161,9 @@ describe('CollectorTraceExporter - node with proto over http', () => {
154161
const callback = args[1];
155162
callback(mockResError);
156163
setTimeout(() => {
157-
const response: any = spyLoggerError.args[0][0];
158-
assert.strictEqual(response, 'statusCode: 400');
164+
const response = spyLoggerError.args[0][0] as string;
159165

166+
assert.ok(response.includes('"code":"400"'));
160167
assert.strictEqual(responseSpy.args[0][0], 1);
161168
done();
162169
});

0 commit comments

Comments
 (0)