Skip to content

Instantly share code, notes, and snippets.

@mjgoeke
Last active November 11, 2021 12:43
Show Gist options
  • Save mjgoeke/fa1579dab734d388591e948e1b1e7421 to your computer and use it in GitHub Desktop.
Save mjgoeke/fa1579dab734d388591e948e1b1e7421 to your computer and use it in GitHub Desktop.
ts-logger examples

example @logger.* decorator usages

@logger.logClass()
export class SimplestExample {
  //all methods except those matching ignorePattern will get wrapped with trace style logging
  ...
}

@logger.logClass()
export class LoginService {

  @logger.transformArgs((username: string, password: string) => [username, '<redacted>']) //redact or modify specific args
  async Login(username: string, password: string): Promise<boolean> {
    ...
  }

  @logger.transformArgs(logger.builtInTransforms.redacted) //alternative to redact all args
  async SomeOtherLogin(username: string, password: string): Promise<boolean> {

  }
  ...
}

@logger.logClass()
export class LockedSQLiteDBConnection {
  txnId?: string;
  constructor(private db: SQLiteDBConnection, private flush: () => Promise<void>, private txnLock = 'txn_lock', private loggingDisabled = false) {}

  @logger.logIf((instance: LockedSQLiteDBConnection) => !instance.loggingDisabled) //filter whether to log based on properties of the class instance at log time
  async executeSet(txnId: string, set: capSQLiteSet[]): Promise<capSQLiteChanges> {
    ...
  }

  @logger.logIf((instance: unknown, description: string, actions: any) => !description.startsWith('internal')) //...and/or by arguments
  async txn(description: string, actions: (txnId: string) => Promise<void>): Promise<void> {
    ...
  }
}

@logger.logClass()
export default class Home extends Vue {
  ...

  @logger.logLevel('info') //increase log level for methods that signify more major events in application
  async sync(): Promise<void> {
    ...
  }
}

@logger.logClass((className, instance: TypedDocumentStore<never, never>) => `${className}<${instance.tableName}>`) //override the class name to include e.g. generics
export class TypedDocumentStore<T extends IdInterface, TIndex extends TIndexType<T>> implements ITypedDocumentStore {
  ...
  @logger.transformResult((r: T[]) => r?.length) //transform logged results for specific cases where e.g. only the array *length* is relevant
  async getAll(): Promise<T[]> {
    ...
  }
}

//note no @logger.logClass()
export class MostlyInternalCalls {
  //many internal functions, excluded from logging
  ...

  @logger.logMethod('MostlyInternalCalls') //opt in to just this method, simpler than @logger.logClass and many @logger.ignore
  public async function MethodRelevantToLog(): Promise<void> {
    ...
  }
}

//logger.log to manually log one-off cases
async uploadAll() : Promise<void> {
  try {
    //some error-able calls here
  } catch (e: unknown) {
    if (e instanceof TypeError && e.message === 'Failed to fetch') {
      //manually log the exceptional case here
      logger.log.warn('Service Unavailable', { className: 'Uploader', methodName: 'uploadAll' });
      //some code to handle error internally
      ...
    }
  }
}

see the end of src/logger.ts for default configuration/conventions, and builtInTransformers

  export const configuration: IConfiguration = {
    sinks: [{ name: 'default console log', write: async (logEntry: ILogEntry): Promise<void> => console[logEntry.logLevel](`warning - logging not configured - ${logEntry.message}`, logEntry.meta) }],
    ignorePattern: fnName => fnName.startsWith('_'),
    transformEachArg: builtInTransforms.arrayToLength(builtInTransforms.maxLength(1000)),
    transformResult: builtInTransforms.arrayToLength(builtInTransforms.maxLength(1000)),
    sinkErrorHandler: async (err: unknown, sinkName: string) => console.error(`error in sink ${sinkName}`, err),
  };

example console sink:

import * as flatted_JSON from 'flatted';
import { logger } from './logger';

export const consoleSink: logger.ISinkWriter = async logEntry => {
  const m = logEntry.meta;
  console[logEntry.logLevel](m.timestamp, logEntry.indent, logEntry.message, m.className, `${m.methodName}(${flatted_JSON.stringify(m.args) ?? ''})`, flatted_JSON.stringify(m.result) ?? '');
};

example (capacitor) text file sink:

import * as C_JSON from 'flatted';
import { Filesystem, ReadFileOptions } from '@capacitor/filesystem';
import { logger } from './logger';

export const textFileSink = (options: ReadFileOptions): logger.ISinkWriter => <logger.ISinkWriter>(async logEntry => {
  const m = logEntry.meta;
  // eslint-disable-next-line max-len
  const line = `${m.timestamp} ${logEntry.logLevel} ${(m.className ?? '').padEnd(30)} ${(m.methodName ?? '').padEnd(20)} ${logEntry.indent} ${(logEntry.message ?? '').padEnd(50)} (${flatted_JSON.stringify(m.args) ?? ''}) ${flatted_JSON.stringify(m.result) ?? ''}`;
  await Filesystem.appendFile({ ...options, ...{ data: `${line}\n` } });
});

example log initialization

/* eslint-disable no-use-before-define */
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
import { logger, loggerConfiguration } from 'ts-logger';
import { textFileSink } from './sink_textFile';
import { consoleSink } from './sink_console';

const textLogFileOptions = { directory: Directory.Data, path: 'log.txt', encoding: Encoding.UTF8 };
const configuration : loggerConfiguration.ISinkLogLevelConfig[] = [
  {
    name: 'textFile',
    out: textFileSink(textLogFileOptions),
    minimumLevel: 'debug',
    overrides: {
      LoginService: 'warn',
    },
  },
  {
    name: 'console',
    out: consoleSink,
    minimumLevel: 'error',
  },
];

async function init() {
  await Filesystem.stat(textLogFileOptions).catch(() => Filesystem.writeFile({ ...textLogFileOptions, ...{ data: '' } })); //if log file doesn't exist, create it
  logger.configuration.sinks = configuration.map(sink => loggerConfiguration.filteredSink(sink));
}

init();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment