Nonvoluntary Professional Mangling

    atrace

    0.11.17 • Public • Published

    atrace

    Application development and telemetry toolkit.

    Features

    • Errors
      • Standardizes error codes
      • Extensible custom errors
      • Causal chain utilities
    • Logging
      • Inferred contextual metadata, including tracing information
      • Structured metadata
      • Compatible with pino
    • Settings
      • Convenient with Node and browser environments (WebPack, ...)
      • Validation for missing and invalid settings with informative errors
      • Tight types inferred with minimal boilerplate
      • Easy to test

    Quickstart

    Errors

    Good error handling is a prerequisite for good logging. To help with this, atrace provides two simple but powerful building blocks.

    Libraries

    The libraryErrorClass function generates a base error class which provides:

    • Namespaced error codes;
    • Causal chains;
    • Optional structured data.
    import {libraryErrorClass} from 'atrace';
    
    // Flexible error class, allowing arbitrary strings as codes.
    const GenericError = libraryErrorClass('my-lib');
    const err1 = new GenericError('SOMETHING_FAILED'); // OK.
    
    // Strongly typed error codes.
    type ErrorID = 'BAR_MISSING' | 'BAZ_BROKEN';
    const SpecificError = libraryErrorClass<ErrorID>('my-lib');
    const err2 = new SpecificError('BAR_MISSING'); // OK.
    const err3 = new SpecificError('SOMETHING_FAILED'); // Fails (unknown ID).
    
    // For even more customization, you can extend the class as usual.
    class CustomError extends libraryErrorClass<ErrorID>('my-lib') {
      constructor(id: ErrorID, someData: number) {
        super(id, {values: {someData}});
      }
      // ...
    }
    const err4 = new CustomError('BAZ_BROKEN', 123);

    Applications

    Beyond the capabilities offered by library errors, applications typically also need the ability to surface statuses back to the client. As a convenience, the standardError function is also available to create application errors from a status.

    import {applicationErrorFactory, standardError} from 'atrace';
    
    // For large applications, prefer to create your own error codes:
    const newError = applicationErrorFactory('bar', {
      'BAD_TOGGLE': 'INVALID_ARGUMENT',
      'BOOM': 'INTERNAL',
    });
    const err1 = newError('BAD_TOGGLE'); // Error with invalid argument status.
    
    // For simple use-cases, you can use the convenience standard errors.
    const err2 = standardError('NOT_FOUND');

    Settings

    import {
      intSetting,
      invalidSource,
      settingsProvider,
      stringSetting,
    } from 'atrace';
    
    const settings = settingsProvider((env) => ({
      port: intSetting(env.PORT ?? 8080), // Integer value
      auth: { // Hierarchical settings
        clientID: stringSetting(env.CLIENT_ID ?? 'my-client'), // String value
        clientSecret: stringSetting({
          source: env.CLIENT_SECRET ?? invalidSource, // Required string value
          sensitive: true, // Redact field from parsed sources
        }),
      },
      tag: stringSetting(env.TAG), // Optional string value
    }));
    
    const val = settings(); // Parsed values

    Importantly, val's type will automatically be inferred as:

    {
      readonly port: number;
      readonly auth: {
        readonly clientID: string;
        readonly clientSecret: string;
      }
      readonly tag: string | undefined;
    }

    If CLIENT_SECRET wasn't defined in the environment, the config provider will throw an informative exception. To test different setting values without needing mocks, pass in a custom environment when calling the config provider.

    const val = settings({
      CLIENT_ID: 'test-id',
      CLIENT_SECRET: 'test-secret',
    });

    Defining a custom setting

    We provide a convenience factory builder for types which do not need any non-standard creation arguments.

    import {simpleSettingFactory} from 'atrace';
    
    // A setting which will have values inferred as `Date`s.
    const dateSetting = simpleSettingFactory((s): Date => new Date(s));

    If additional arguments are desired, it's always possible--just more verbose--to use the underlying setting factory directly. For example, here's one way to implement an enum setting:

    import {newSetting, Setting, SettingParams, SettingSource} from 'atrace';
    
    /** Enum setting creation parameters. */
    export interface EnumSettingParams<E extends string, S>
      extends SettingParams<S> {
      /** Allowed enum values. */
      readonly symbols: ReadonlyArray<E>;
    }
    
    /** Creates a new enum setting. */
    export function enumSetting<E extends string, S extends SettingSource>(
      params: EnumSettingParams<E, S>
    ): Setting<S, E> {
      return newSetting(params, (s): any => {
        if (!~params.symbols.indexOf(s as any)) {
          throw new Error('Invalid enum value');
        }
        return s;
      });
    }

    Recommended project structure

    In production code, we recommend using modular configuration types. Performing post-processing on setting values is easy with a settingsManager:

    // src/config.ts
    import {settingsManager} from 'atrace';
    
    // First the (private) underlying settings resource manager.
    const withSettings = settingsManager((env) => ({/* ... */}));
    
    // Then the (public) configuration types. These can be different from the raw
    // settings for example discriminated unions are often handy. These are meant
    // to be used everywhere in the application. Smaller, more specific, types will
    // make testing easier and increase modularity.
    export interface ModuleAConfig {/* ... */}
    export interface ModuleBConfig {/* ... */}
    export interface Config {
      readonly moduleA: ModuleAConfig;
      readonly moduleB: ModuleBConfig;
      // ...
    }
    
    // Now the (public, though not meant for use everywhere - see below)
    // configuration factory. This function should only be imported in the
    // application's entry point and this module's unit-tests. Other modules should
    // accept already instantiated configuration types, making testing much easier
    // and dependencies explicit.
    export const config = withSettings((val) => {
      // Transform the setting values into the public configuration types...
      return {/* ... */};
    });
    // test/config.test.ts
    import * as sut from '../src/config';
    
    // Within a test...
    const env = {/* ... */}; // Test environment.
    expect(config(env)).toEqual(/* ... */); // Expected config for the environment.

    Keywords

    none

    Install

    npm i atrace

    DownloadsWeekly Downloads

    8

    Version

    0.11.17

    License

    MIT

    Unpacked Size

    115 kB

    Total Files

    28

    Last publish

    Collaborators

    • mtth