Skip to content

Commit

Permalink
feat: add global error handler (#1514)
Browse files Browse the repository at this point in the history
  • Loading branch information
mwear authored Oct 7, 2020
1 parent 60d4dab commit 240f852
Show file tree
Hide file tree
Showing 29 changed files with 582 additions and 155 deletions.
6 changes: 3 additions & 3 deletions packages/opentelemetry-api/src/common/Exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@
*/

interface ExceptionWithCode {
code: string;
code: string | number;
name?: string;
message?: string;
stack?: string;
}

interface ExceptionWithMessage {
code?: string;
code?: string | number;
message: string;
name?: string;
stack?: string;
}

interface ExceptionWithName {
code?: string;
code?: string | number;
message?: string;
name: string;
stack?: string;
Expand Down
40 changes: 40 additions & 0 deletions packages/opentelemetry-core/src/common/global-error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.
*/

import { Exception } from '@opentelemetry/api';
import { loggingErrorHandler } from './logging-error-handler';
import { ErrorHandler } from './types';

/** The global error handler delegate */
let delegateHandler = loggingErrorHandler();

/**
* Set the global error handler
* @param {ErrorHandler} handler
*/
export function setGlobalErrorHandler(handler: ErrorHandler) {
delegateHandler = handler;
}

/**
* Return the global error handler
* @param {Exception} ex
*/
export const globalErrorHandler = (ex: Exception) => {
try {
delegateHandler(ex);
} catch {} // eslint-disable-line no-empty
};
66 changes: 66 additions & 0 deletions packages/opentelemetry-core/src/common/logging-error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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.
*/

import { Logger, Exception } from '@opentelemetry/api';
import { ConsoleLogger } from './ConsoleLogger';
import { ErrorHandler, LogLevel } from './types';

/**
* Returns a function that logs an error using the provided logger, or a
* console logger if one was not provided.
* @param {Logger} logger
*/
export function loggingErrorHandler(logger?: Logger): ErrorHandler {
logger = logger ?? new ConsoleLogger(LogLevel.ERROR);
return (ex: Exception) => {
logger!.error(stringifyException(ex));
};
}

/**
* Converts an exception into a string representation
* @param {Exception} ex
*/
function stringifyException(ex: Exception | string): string {
if (typeof ex === 'string') {
return ex;
} else {
return JSON.stringify(flattenException(ex));
}
}

/**
* Flattens an exception into key-value pairs by traversing the prototype chain
* and coercing values to strings. Duplicate properties will not be overwritten;
* the first insert wins.
*/
function flattenException(ex: Exception): Record<string, string> {
const result = {} as Record<string, string>;
let current = ex;

while (current !== null) {
Object.getOwnPropertyNames(current).forEach(propertyName => {
if (result[propertyName]) return;
const value = current[propertyName as keyof typeof current];
if (value) {
result[propertyName] = String(value);
}
});
current = Object.getPrototypeOf(current);
}

return result;
}
6 changes: 6 additions & 0 deletions packages/opentelemetry-core/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Exception } from '@opentelemetry/api';

export enum LogLevel {
ERROR,
WARN,
Expand Down Expand Up @@ -56,3 +59,6 @@ export interface InstrumentationLibrary {
readonly name: string;
readonly version: string;
}

/** Defines an error handler function */
export type ErrorHandler = (ex: Exception) => void;
2 changes: 2 additions & 0 deletions packages/opentelemetry-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

export * from './common/attributes';
export * from './common/ConsoleLogger';
export * from './common/global-error-handler';
export * from './common/logging-error-handler';
export * from './common/NoopLogger';
export * from './common/time';
export * from './common/types';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.
*/

import * as assert from 'assert';
import * as sinon from 'sinon';
import { globalErrorHandler, setGlobalErrorHandler } from '../../src';
import { Exception } from '@opentelemetry/api';

describe('globalErrorHandler', () => {
let defaultHandler: sinon.SinonSpy;

beforeEach(() => {
defaultHandler = sinon.spy();
setGlobalErrorHandler(defaultHandler);
});

it('receives errors', () => {
const err = new Error('this is bad');
globalErrorHandler(err);
sinon.assert.calledOnceWithExactly(defaultHandler, err);
});

it('replaces delegate when handler is updated', () => {
const err = new Error('this is bad');
const newHandler = sinon.spy();
setGlobalErrorHandler(newHandler);

globalErrorHandler(err);

sinon.assert.calledOnceWithExactly(newHandler, err);
sinon.assert.notCalled(defaultHandler);
});

it('catches exceptions thrown in handler', () => {
setGlobalErrorHandler((ex: Exception) => {
throw new Error('bad things');
});

assert.doesNotThrow(() => {
globalErrorHandler('an error');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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.
*/

import * as assert from 'assert';
import * as sinon from 'sinon';
import { ErrorHandler, loggingErrorHandler } from '../../src';

describe('loggingErrorHandler', () => {
let handler: ErrorHandler;
const errorStub = sinon.fake();

beforeEach(() => {
handler = loggingErrorHandler({
debug: sinon.fake(),
info: sinon.fake(),
warn: sinon.fake(),
error: errorStub,
});
});

it('logs from string', () => {
const err = 'not found';
handler(err);
assert.ok(errorStub.calledOnceWith(err));
});

it('logs from an object', () => {
const err = {
name: 'NotFoundError',
message: 'not found',
randomString: 'random value',
randomNumber: 42,
randomArray: [1, 2, 3],
randomObject: { a: 'a' },
stack: 'a stack',
};

handler(err);

const [result] = errorStub.lastCall.args;

assert.ok(result.includes(err.name));
assert.ok(result.includes(err.message));
assert.ok(result.includes(err.randomString));
assert.ok(result.includes(err.randomNumber));
assert.ok(result.includes(err.randomArray));
assert.ok(result.includes(err.randomObject));
assert.ok(result.includes(JSON.stringify(err.stack)));
});

it('logs from an error', () => {
const err = new Error('this is bad');

handler(err);

const [result] = errorStub.lastCall.args;

assert.ok(result.includes(err.name));
assert.ok(result.includes(err.message));
assert.ok(result.includes(JSON.stringify(err.stack)));
});
});
4 changes: 1 addition & 3 deletions packages/opentelemetry-exporter-collector-proto/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ export function send<ExportItem, ServiceRequest>(
);
}
} else {
onError({
message: 'No proto',
});
onError(new collectorTypes.CollectorExporterError('No proto'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,14 @@ describe('CollectorMetricExporter - node with proto over http', () => {
});

it('should log the error message', done => {
const spyLoggerError = sinon.stub(collectorExporter.logger, 'error');
const spyLoggerError = sinon.spy();
const handler = core.loggingErrorHandler({
debug: sinon.fake(),
info: sinon.fake(),
warn: sinon.fake(),
error: spyLoggerError,
});
core.setGlobalErrorHandler(handler);

const responseSpy = sinon.spy();
collectorExporter.export(metrics, responseSpy);
Expand All @@ -187,9 +194,8 @@ describe('CollectorMetricExporter - node with proto over http', () => {
const callback = args[1];
callback(mockResError);
setTimeout(() => {
const response: any = spyLoggerError.args[0][0];
assert.strictEqual(response, 'statusCode: 400');

const response = spyLoggerError.args[0][0] as string;
assert.ok(response.includes('"code":"400"'));
assert.strictEqual(responseSpy.args[0][0], 1);
done();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,14 @@ describe('CollectorTraceExporter - node with proto over http', () => {
});

it('should log the error message', done => {
const spyLoggerError = sinon.stub(collectorExporter.logger, 'error');
const spyLoggerError = sinon.spy();
const handler = core.loggingErrorHandler({
debug: sinon.fake(),
info: sinon.fake(),
warn: sinon.fake(),
error: spyLoggerError,
});
core.setGlobalErrorHandler(handler);

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

assert.ok(response.includes('"code":"400"'));
assert.strictEqual(responseSpy.args[0][0], 1);
done();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
*/

import { Attributes, Logger } from '@opentelemetry/api';
import { ExportResult, NoopLogger } from '@opentelemetry/core';
import {
ExportResult,
NoopLogger,
globalErrorHandler,
} from '@opentelemetry/core';
import {
CollectorExporterError,
CollectorExporterConfigBase,
Expand Down Expand Up @@ -75,9 +79,7 @@ export abstract class CollectorExporterBase<
resultCallback(ExportResult.SUCCESS);
})
.catch((error: ExportServiceError) => {
if (error.message) {
this.logger.error(error.message);
}
globalErrorHandler(error);
if (error.code && error.code < 500) {
resultCallback(ExportResult.FAILED_NOT_RETRYABLE);
} else {
Expand Down
Loading

0 comments on commit 240f852

Please sign in to comment.