{Context,Chained}Error: New modules
This will allow us to provide additional useful information for structure logging.master
parent
1f66a25658
commit
fe10578949
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Uniform error handling for Liza: error chaining
|
||||
*
|
||||
* Copyright (C) 2010-2019 R-T Specialty, LLC.
|
||||
*
|
||||
* This file is part of the Liza Data Collection Framework.
|
||||
*
|
||||
* liza is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ___Writable } from 'naughty';
|
||||
|
||||
|
||||
/**
|
||||
* An Error augmented to include information about an underlying cause
|
||||
*
|
||||
* To create new chains, use the `chain` function.
|
||||
*
|
||||
* Chaining should be used when an error is caught and transformed into
|
||||
* another, more specific error. By maintaining a reference to an existing
|
||||
* error, context is not lost, which can be helpful for debugging and
|
||||
* logging.
|
||||
*
|
||||
* Chains may be nested to an arbitrary depth, but because of the nature of
|
||||
* JavaScript's errors, recursive chain type checks must be done at runtime.
|
||||
*/
|
||||
export interface ChainedError<T extends Error = Error> extends Error
|
||||
{
|
||||
readonly chain: T,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Type predicate for `ChainedError`
|
||||
*
|
||||
* This predicate can be used at runtime to determine whether an error is
|
||||
* chained.
|
||||
*
|
||||
* @param e error object
|
||||
*
|
||||
* @return whether `e` is of type ChainedError
|
||||
*/
|
||||
export const isChained = ( e: Error ): e is ChainedError =>
|
||||
( <ChainedError>e ).chain !== undefined;
|
||||
|
||||
|
||||
/**
|
||||
* Chains two `Error`s
|
||||
*
|
||||
* This is intended to be used as if it were a constructor, where the first
|
||||
* argument is a new `Error` instance.
|
||||
*
|
||||
* @param enew new error
|
||||
* @param eprev error to chain
|
||||
*
|
||||
* @return `enew` with `eprev` chained
|
||||
*/
|
||||
export function chain( enew: Error, eprev: Error ): ChainedError
|
||||
{
|
||||
( <___Writable<ChainedError>>enew ).chain = eprev;
|
||||
return <ChainedError>enew;
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* Uniform error handling for Liza: error context
|
||||
*
|
||||
* Copyright (C) 2010-2019 R-T Specialty, LLC.
|
||||
*
|
||||
* This file is part of the Liza Data Collection Framework.
|
||||
*
|
||||
* liza is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This context system is intended to play nicely with how Error objects are
|
||||
* typically used in JavaScript. As such, rather than creating new error
|
||||
* prototypes / subclasses, these rely on structural typing to augment
|
||||
* existing `Error` objects.
|
||||
*/
|
||||
|
||||
import { ___Writable } from 'naughty';
|
||||
|
||||
|
||||
/**
|
||||
* Error with additional context regarding its cause
|
||||
*
|
||||
* Errors may be augmented with key/value data (see `ErrorContext`)
|
||||
* containing data that will be helpful for debugging the cause of the
|
||||
* error. The context should be expected to appear in structured logs, so
|
||||
* it shouldn't include sensitive data without some mitigation layer.
|
||||
*
|
||||
* A context may be optionally typed, but note that such context will
|
||||
* generally be lost any time promises are used, so type predicates will
|
||||
* need to be used to restore the type with information at runtime.
|
||||
*
|
||||
* Since the context is intended primarily for debugging, it shouldn't be
|
||||
* relied on to drive control flow unless absolutely necessary, in which
|
||||
* case an explicit context should be used.
|
||||
*/
|
||||
export interface ContextError<T extends ErrorContext = ErrorContext>
|
||||
extends Error
|
||||
{
|
||||
readonly context: T,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Key/value context for an error
|
||||
*
|
||||
* Rather than accepting data of an arbitrary type, we force key/value for
|
||||
* reasons of extensibility and consistency: if more information is needed
|
||||
* in the future, the type will remain unchanged. The values, however, may
|
||||
* include any arbitrary data.
|
||||
*/
|
||||
export type ErrorContext = { readonly [P: string]: any };
|
||||
|
||||
|
||||
/**
|
||||
* Type predicate for `ContextError`
|
||||
*
|
||||
* Note that this is a predicate for a generic `ContextError`, type, which
|
||||
* is equivalent to `ContextError<ErrorContext>`. Other contexts must
|
||||
* define their own predicates.
|
||||
*
|
||||
* @param e error object
|
||||
*
|
||||
* @return whether `e` is of type `ContextError`
|
||||
*/
|
||||
export const hasContext = ( e: Error ): e is ContextError =>
|
||||
( <ContextError>e ).context !== undefined;
|
||||
|
||||
|
||||
/**
|
||||
* Adds context to an error
|
||||
*
|
||||
* This is intended to be used as if it were a constructor, where the first
|
||||
* argument is a new `Error` instance.
|
||||
*
|
||||
* @param enew error object to add context to
|
||||
* @param context key/value context information
|
||||
*/
|
||||
export function context<T = ErrorContext>( enew: Error, context: T ):
|
||||
ContextError<T>
|
||||
{
|
||||
( <___Writable<ContextError<T>>>enew ).context = context;
|
||||
return <ContextError<T>>enew;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* Things that should only be used when absolutely necessary
|
||||
*
|
||||
* Copyright (C) 2010-2019 R-T Specialty, LLC.
|
||||
*
|
||||
* This file is part of the Liza Data Collection Framework.
|
||||
*
|
||||
* liza is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* DEFINITIONS IN THIS PACKAGE DO NAUGHTY THINGS THAT CIRCUMVENT TYPE
|
||||
* SAFETY; THEY SHOULD BE USED ONLY WHEN NECESSARY, AND ONLY WHEN YOU KNOW
|
||||
* WHAT YOU'RE DOING, SINCE THEY MAY INTRODUCE BUGS!
|
||||
*
|
||||
* The prefix `___` is added to each of the names here so that code can be
|
||||
* easily searched for uses of naughty things.
|
||||
*
|
||||
* These types are also exported, unlike some other `.d.ts` files which are
|
||||
* universally available during complication---this forces the importing of
|
||||
* this file, named `naughty.d.ts`, which should raise some eyebrows and
|
||||
* make people less likely to copy existing code that uses it.
|
||||
*/
|
||||
|
||||
declare module 'naughty'
|
||||
{
|
||||
/**
|
||||
* Make type `T` writable while otherwise maintaining type safety
|
||||
*
|
||||
* _Only use this generic if you are the owner of the object being
|
||||
* manipulated!__
|
||||
*
|
||||
* This should be used when we want types to be readonly, but we need to
|
||||
* be able to modify an existing object to initialize the
|
||||
* properties. This should only be used in situations where it's not
|
||||
* feasible to add those properties when the object is first created.
|
||||
*/
|
||||
export type ___Writable<T> = { -readonly [K in keyof T]: T[K] };
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* Tests error chaining
|
||||
*
|
||||
* Copyright (C) 2010-2019 R-T Specialty, LLC.
|
||||
*
|
||||
* This file is part of the Liza Data Collection Framework.
|
||||
*
|
||||
* liza is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as sut from "../../src/error/ChainedError";
|
||||
import { expect } from 'chai';
|
||||
|
||||
|
||||
describe( 'ChainedError', () =>
|
||||
{
|
||||
it( "can be created with generic error", () =>
|
||||
{
|
||||
const eprev = new Error( "previous error" );
|
||||
|
||||
expect( sut.chain( new Error( "new error" ), eprev ).chain )
|
||||
.to.equal( eprev );
|
||||
} );
|
||||
|
||||
|
||||
it( "can be chained to arbitrary depth", () =>
|
||||
{
|
||||
const e1 = new Error( "lower" );
|
||||
const e2 = sut.chain( new Error( "mid" ), e1 );
|
||||
const e3 = sut.chain( new Error( "outer" ), e2 );
|
||||
|
||||
expect( sut.isChained( e3 ) ).to.be.true;
|
||||
expect( sut.isChained( e2 ) ).to.be.true;
|
||||
expect( sut.isChained( e1 ) ).to.be.false;
|
||||
} );
|
||||
|
||||
|
||||
it( "provides type predicate for TypeScript", () =>
|
||||
{
|
||||
const inner = new Error( "inner" );
|
||||
|
||||
// force to Error to discard ChainedError type
|
||||
const outer: Error = sut.chain( new Error( "outer" ), inner );
|
||||
|
||||
if ( sut.isChained( outer ) )
|
||||
{
|
||||
// if isChained was properly defined, then outer should now
|
||||
// have type ChainedError, and so this should compile
|
||||
expect( outer.chain ).to.equal( inner );
|
||||
}
|
||||
else
|
||||
{
|
||||
expect.fail();
|
||||
}
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Tests error context
|
||||
*
|
||||
* Copyright (C) 2010-2019 R-T Specialty, LLC.
|
||||
*
|
||||
* This file is part of the Liza Data Collection Framework.
|
||||
*
|
||||
* liza is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as sut from "../../src/error/ContextError";
|
||||
import { expect } from 'chai';
|
||||
|
||||
|
||||
describe( 'ContextError', () =>
|
||||
{
|
||||
it( "can be created with generic error", () =>
|
||||
{
|
||||
const context = { foo: "context" };
|
||||
|
||||
expect( sut.context( new Error( "test error" ), context ).context )
|
||||
.to.equal( context );
|
||||
} );
|
||||
|
||||
|
||||
it( "provides type predicate for TypeScript", () =>
|
||||
{
|
||||
const context = { bar: "baz context" };
|
||||
|
||||
// force to Error to discard ContextError type
|
||||
const e: Error = sut.context( new Error( "test error" ), context );
|
||||
|
||||
if ( sut.hasContext( e ) )
|
||||
{
|
||||
// if isChained was properly defined, then outer should now
|
||||
// have type ChainedError, and so this should compile
|
||||
expect( e.context ).to.equal( context );
|
||||
}
|
||||
else
|
||||
{
|
||||
expect.fail();
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
it( "can create typed contexts", () =>
|
||||
{
|
||||
type FooErrorContext = { foo: string };
|
||||
|
||||
// this is the actual test
|
||||
const e: sut.ContextError<FooErrorContext> =
|
||||
sut.context( new Error( "test error" ), { foo: "context" } );
|
||||
|
||||
// contravariance check (would fail to compile)
|
||||
expect( sut.hasContext( e ) ).to.be.true;
|
||||
} );
|
||||
} );
|
Loading…
Reference in New Issue