Skip to content

Commit

Permalink
Add Logger singleton class
Browse files Browse the repository at this point in the history
  • Loading branch information
zajrik committed Apr 9, 2017
1 parent fa2317c commit d6304b8
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 11 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@
"discordapp"
],
"dependencies": {
"chalk": "^1.1.3",
"discord.js": "^11.0.0",
"glob": "7.1.1",
"node-json-db": "0.7.3",
"performance-now": "^2.0.0",
"postinstall-build": "^2.1.3"
},
"devDependencies": {
"@types/chalk": "^0.4.31",
"@types/glob": "^5.0.30",
"@types/node": "^7.0.8",
"@types/node-json-db": "0.0.1",
Expand Down
14 changes: 12 additions & 2 deletions src/client/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ClientStorage } from '../types/ClientStorage';
import { MiddlewareFunction } from '../types/MiddlewareFunction';
import { StorageProviderConstructor } from '../types/StorageProviderConstructor';
import { BaseCommandName } from '../types/BaseCommandName';
import { Logger, logger } from '../util/logger/Logger';

/**
* The YAMDBF Client through which you can access [storage]{@link Client#storage}
Expand All @@ -24,6 +25,7 @@ import { BaseCommandName } from '../types/BaseCommandName';
*/
export class Client extends Discord.Client
{
@logger private logger: Logger;
public name: string;
public commandsDir: string;
public statusText: string;
Expand Down Expand Up @@ -83,7 +85,7 @@ export class Client extends Discord.Client
* @name Client#readyText
* @type {string}
*/
this.readyText = options.readyText || 'Ready!';
this.readyText = options.readyText || 'Client ready!';

/**
* Whether or not a generic 'command not found' message
Expand Down Expand Up @@ -142,6 +144,10 @@ export class Client extends Discord.Client
if (options.ratelimit)
this._rateLimiter = new RateLimiter(options.ratelimit, true);

// Set the logger level if provided
if (typeof options.logLevel !== 'undefined')
this.logger.setLogLevel(options.logLevel);

// Middleware function storage for the client instance
this._middleware = [];

Expand Down Expand Up @@ -242,7 +248,11 @@ export class Client extends Discord.Client
this.emit('waiting');
});

this.once('finished', () => this.emit('clientReady'));
this.once('finished', () =>
{
this.logger.log('Client', this.readyText);
this.emit('clientReady');
});

this.on('guildCreate', () =>
{
Expand Down
13 changes: 8 additions & 5 deletions src/command/CommandLoader.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import * as glob from 'glob';
import * as path from 'path';

import { Client } from '../client/Client';
import { Command } from './Command';
import { CommandRegistry } from './CommandRegistry';
import { BaseCommandName } from '../types/BaseCommandName';
import { Logger, logger } from '../util/logger/Logger';

/**
* Handles loading all commands from the given Client's commandsDir
* @private
*/
export class CommandLoader<T extends Client>
{
@logger private logger: Logger;
private _client: T;
public constructor(client: T)
{
Expand Down Expand Up @@ -47,16 +48,18 @@ export class CommandLoader<T extends Client>
throw new Error(`Command "${_command.overloads}" does not exist to be overloaded.`);
this._client.commands.delete(_command.overloads);
this._client.commands.register(_command, _command.name);
console.log(`Command '${_command.name}' loaded, overloading command '${_command.overloads}'.`);
this.logger.info('CommandLoader',
`Command '${_command.name}' loaded, overloading command '${_command.overloads}'.`);
}
else
{
this._client.commands.register(_command, _command.name);
loadedCommands++;
console.log(`Command '${_command.name}' loaded.`);
this.logger.info('CommandLoader', `Command '${_command.name}' loaded.`);
}
}
console.log(`Loaded ${loadedCommands} total commands in ${this._client.commands.groups.length} groups.`);
this.logger.info('CommandLoader',
`Loaded ${loadedCommands} total commands in ${this._client.commands.groups.length} groups.`);
}

/**
Expand All @@ -74,7 +77,7 @@ export class CommandLoader<T extends Client>
const _command: Command<T> = new loadedCommandClass(this._client);
_command._classloc = commandLocation;
this._client.commands.register(_command, _command.name, true);
console.log(`Command '${_command.name}' reloaded.`);
this.logger.info('CommandLoader', `Command '${_command.name}' reloaded.`);
return true;
}

Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ export { CommandDecorators }

export { Time } from './util/Time';
export { Util } from './util/Util';
export { Logger } from './util/logger/Logger';
export { logger } from './util/logger/LoggerDecorator';
export { LogLevel } from './types/LogLevel';

export { ArgOpts } from './types/ArgOpts';
export { BaseCommandName } from './types/BaseCommandName';
export { YAMDBFOptions } from './types/YAMDBFOptions';
export { ClientStorage } from './types/ClientStorage';
export { CommandInfo } from './types/CommandInfo';
export { DefaultGuildSettings } from './types/DefaultGuildSettings';
Expand All @@ -37,6 +39,7 @@ export { Message } from './types/Message';
export { MiddlewareFunction } from './types/MiddlewareFunction';
export { ResolveArgType } from './types/ResolveArgType';
export { StorageProviderConstructor } from './types/StorageProviderConstructor';
export { YAMDBFOptions } from './types/YAMDBFOptions';

export const version: string = require(path.join(__dirname, '..', 'package')).version;

Expand Down
25 changes: 25 additions & 0 deletions src/types/LogLevel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @typedef {enum} LogLevel Enum containing the different levels of
* potential logger output. Each level represents itself and everything
* above it in the enum. The default logger log level is `LogLevel.LOG`
* ```
* enum LogLevel
* {
* NONE,
* LOG,
* INFO,
* WARN,
* ERROR.
* DEBUG
* }
* ```
*/
export enum LogLevel
{
NONE,
LOG,
INFO,
WARN,
ERROR,
DEBUG
}
2 changes: 2 additions & 0 deletions src/types/YAMDBFOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import { StorageProviderConstructor } from './StorageProviderConstructor';
import { BaseCommandName } from './BaseCommandName';
import { LogLevel } from './LogLevel';

export type YAMDBFOptions = {
name: string;
Expand All @@ -33,4 +34,5 @@ export type YAMDBFOptions = {
disableBase?: BaseCommandName[];
ratelimit?: string;
config: any;
logLevel?: LogLevel;
};
172 changes: 172 additions & 0 deletions src/util/logger/Logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { LogLevel } from '../../types/LogLevel';
export { logger } from './LoggerDecorator';
import * as chalk from 'chalk';

/**
* Singleton containing methods for asynchronous logging with clean,
* configurable output
*
* Easiest manner of use is via the `@logger` parameter decorator
* to attach the logger to a class property for use within that class.
* Otherwise the singleton instance can be accessed via `Logger.instance()`
*
* Logging can be turned off by setting the logging level to `LogLevel.NONE`
*/
export class Logger
{
private static _instance: Logger;
private _logLevel: LogLevel;
private constructor()
{
if (Logger._instance)
throw new Error('Cannot create multiple instances of Logger singleton');
Logger._instance = this;
this._logLevel = 1;
}

/**
* `LogLevel.NONE` enum shortcut
* @name Logger.NONE
* @type {LogLevel}
*/
public static readonly NONE: LogLevel = LogLevel.NONE;

/**
* `LogLevel.LOG` enum shortcut
* @name Logger.LOG
* @type {LogLevel}
*/
public static readonly LOG: LogLevel = LogLevel.LOG;

/**
* `LogLevel.INFO` enum shortcut
* @name Logger.INFO
* @type LogLevel
*/
public static readonly INFO: LogLevel = LogLevel.INFO;

/**
* `LogLevel.WARN` enum shortcut
* @name Logger.WARN
* @type {LogLevel}
*/
public static readonly WARN: LogLevel = LogLevel.WARN;

/**
* `LogLevel.ERROR` enum shortcut
* @name Logger.ERROR
* @type {LogLevel}
*/
public static readonly ERROR: LogLevel = LogLevel.ERROR;

/**
* `LogLevel.DEBUG` enum shortcut
* @name Logger.DEBUG
* @type LogLevel
*/
public static readonly DEBUG: LogLevel = LogLevel.DEBUG;

/**
* Returns the Logger singleton instance
* @method Logger.instance
* @returns {Logger}
*/
public static instance(): Logger
{
if (!Logger._instance) return new Logger();
else return Logger._instance;
}

/**
* Set the level of output that will be logged
* @method Logger#setLogLevel
* @param {LogLevel} level The level of logging to output
*/
public setLogLevel(level: LogLevel): void
{
this._logLevel = level;
}

/**
* Log to the console. This is the base level of logging and is the default
* log level, represented by `LogLevel.LOG`, when the logger singleton is created
* @method Logger#log
* @param {string} tag Tag to prefix the log with
* @param {...string} text String(s) to log
*/
public async log(tag: string, ...text: string[]): Promise<void>
{
if (this._logLevel < LogLevel.LOG) return;
this._write(chalk.green('LOG'), tag, text.join(' '));
}

/**
* Log information that doesn't need to be visible by default to the console.
* Will not be logged unless the logging level is `LogLevel.INFO` or higher
* @method Logger#info
* @param {string} tag Tag to prefix the log with
* @param {...string} text String(s) to log
*/
public async info(tag: string, ...text: string[]): Promise<void>
{
if (this._logLevel < LogLevel.INFO) return;
this._write(chalk.blue('INFO'), tag, text.join(' '));
}

/**
* Log warning text to the console.
* Will not be logged unless the logging level is `LogLevel.WARN` or higher
* @method Logger#warn
* @param {string} tag Tag to prefix the log with
* @param {...string} text String(s) to log
*/
public async warn(tag: string, ...text: string[]): Promise<void>
{
if (this._logLevel < LogLevel.WARN) return;
this._write(chalk.yellow('WARN'), tag, text.join(' '));
}

/**
* Log error text to the console.
* Will not be logged unless the logging level is `LogLevel.ERROR` or higher
* @method Logger#error
* @param {string} tag Tag to prefix the log with
* @param {...string} text String(s) to log
*/
public async error(tag: string, ...text: string[]): Promise<void>
{
if (this._logLevel < LogLevel.ERROR) return;
this._write(chalk.red('ERROR'), tag, text.join(' '));
}

/**
* Log error text to the console.
* Will not be logged unless the logging level is `LogLevel.ERROR` or higher
* @method Logger#error
* @param {string} tag Tag to prefix the log with
* @param {...string} text String(s) to log
*/
public async debug(tag: string, ...text: string[]): Promise<void>
{
if (this._logLevel < LogLevel.DEBUG) return;
this._write(chalk.magenta('DEBUG'), tag, text.join(' '));
}

/**
* Write to the console
* @private
*/
private _write(type: string, tag: string, text: string): void
{
const d: Date = new Date();
const hours: number = d.getHours();
const minutes: number = d.getMinutes();
const seconds: number = d.getSeconds();
const timestamp: string = `${
hours < 10 ? `0${hours}` : hours}:${
minutes < 10 ? `0${minutes}` : minutes}:${
seconds < 10 ? `0${seconds}` : seconds}`;

process.stdout.write(`[${chalk.grey(timestamp)}][${type}][${chalk.cyan(tag)}]: ${text}\n`);
}
}
48 changes: 48 additions & 0 deletions src/util/logger/LoggerDecorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Logger } from './Logger';

/**
* Represents a type that has a `logger` property
* containing the Logger singleton instance
*/
// export type Loggable<T> = T & { logger: Logger };

/**
* Class decorator that transforms a class to Loggable<T>.
* Regrettably works but is not properly picked up by intellisense
* at this point in time, meaning compiler errors when attempting
* to access the `logger` property on the decorated class.
* Maybe someday.
*
* Example:
* ```
* &#64loggable
* class Foo { }
* ```
*/
// export function loggable<T extends Function>(target: T): Loggable<T>
// {
// Object.defineProperty(target.prototype, 'logger',
// { value: Logger.instance });
// return <Loggable<T>> target;
// }

/**
* Property decorator that will automatically assign
* the Logger singleton instance to the decorated
* class property
*
* Example:
* ```
* class Foo {
* &#64logger private logger: Logger;
* ...
* ```
* **Note:** This is a Typescript feature. If using the logger is desired
* in Javascript you should simply retrieve the singleton instance via
* `Logger.instance()`
*/
export function logger<T>(target: T, key: string): void
{
Object.defineProperty(target, 'logger',
{ value: Logger.instance() });
}
Loading

0 comments on commit d6304b8

Please sign in to comment.