Skip to content

Commit

Permalink
feat: extended logging support (#603)
Browse files Browse the repository at this point in the history
* feat: extended logging support

* test: add logger tests

* refactor(clouds): replace console with a logger

* refactor: check log level value

* chore: update examples

* docs: add logger options
  • Loading branch information
kukhariev authored Aug 12, 2022
1 parent c25d51a commit ce9d5a0
Show file tree
Hide file tree
Showing 21 changed files with 419 additions and 48 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ Some available options: :
| `userIdentifier` | `UserIdentifier` | | _Get user identity_ |
| `onComplete` | `OnComplete` | | _On upload complete callback_ |
| `expiration` | `ExpirationOptions` | | _Configuring the cleanup of abandoned and completed uploads_ |
| `logger` | `Logger` | | _Custom logger injection_ |
| `logLevel` | `LogLevel` | `"none"` | _Set built-in logger severity level_ |

## Contributing

Expand Down
3 changes: 1 addition & 2 deletions examples/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Enable debug log
DEBUG=uploadx:*
LOG_LEVEL=debug

PORT=3002

Expand Down
3 changes: 2 additions & 1 deletion examples/express-basic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DiskStorageOptions, uploadx } from '@uploadx/core';
import { DiskStorageOptions, LogLevel, uploadx } from '@uploadx/core';
import * as express from 'express';

const PORT = process.env.PORT || 3002;
Expand All @@ -12,6 +12,7 @@ const opts: DiskStorageOptions = {
useRelativeLocation: true,
filename: file => file.originalName,
expiration: { maxAge: '1h', purgeInterval: '10min' },
logLevel: <LogLevel>process.env.LOG_LEVEL || 'info',
onComplete: file => {
console.log('File upload complete: ', file);
return file;
Expand Down
6 changes: 4 additions & 2 deletions examples/express-s3.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as express from 'express';
import { uploadx } from '@uploadx/core';
import { LogLevel, uploadx } from '@uploadx/core';
import { S3Storage } from '@uploadx/s3';

const PORT = process.env.PORT || 3002;
Expand All @@ -22,7 +22,9 @@ const storage = new S3Storage({
endpoint: 'http://127.0.0.1:9000',
forcePathStyle: true,
expiration: { maxAge: '1h', purgeInterval: '15min' },
onComplete: file => console.log('File upload complete: ', file)
onComplete: file => console.log('File upload complete: ', file),
// logger: console
logLevel: <LogLevel>process.env.LOG_LEVEL || 'info'
});

app.use('/files', uploadx({ storage }));
Expand Down
1 change: 0 additions & 1 deletion examples/express-tus.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import * as express from 'express';
import { DiskFile, DiskStorageOptions, tus } from '@uploadx/core';

Expand Down
12 changes: 10 additions & 2 deletions examples/express.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { DiskFile, uploadx } from '@uploadx/core';
import * as express from 'express';
import { createLogger, format, transports } from 'winston';

const logger = createLogger({
format: format.combine(format.splat(), format.simple()),
transports: [new transports.Console()],
level: 'info'
});

const PORT = process.env.PORT || 3002;
type UserInfo = { user?: { id: string; email: string } };
Expand All @@ -27,9 +34,10 @@ app.use(
uploadx.upload({
directory: 'upload',
expiration: { maxAge: '1h', purgeInterval: '10min' },
userIdentifier: (req: express.Request & UserInfo) => `${req.user!.id}-${req.user!.email}`
userIdentifier: (req: express.Request & UserInfo) => `${req.user!.id}-${req.user!.email}`,
logger
}),
onComplete
);

app.listen(PORT, () => console.log('listening on port:', PORT));
app.listen(PORT, () => logger.info('listening on port: %d', PORT));
5 changes: 4 additions & 1 deletion examples/fastify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import fastify from 'fastify';
import middie from 'middie';
import { Uploadx } from '@uploadx/core';
import { join } from 'path';
import pino from 'pino';

const logger = pino({ level: process.env.LOG_LEVEL || 'info', name: 'uploadx' });

const PORT = process.env.PORT || 3002;

const server = fastify({ logger: true });
const uploadx = new Uploadx({ directory: 'upload' });
const uploadx = new Uploadx({ directory: 'upload', logger });
uploadx.on('completed', ({ name, originalName }) =>
server.log.info(
`upload complete, path: ${join('files', name)}, original filename: ${originalName}`
Expand Down
4 changes: 3 additions & 1 deletion examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"fastify": "3.29.0",
"middie": "6.1.0",
"node-uploadx": "^5.1.5",
"ts-node-dev": "2.0.0"
"pino": "8.4.0",
"ts-node-dev": "2.0.0",
"winston": "3.8.1"
},
"devDependencies": {
"@types/express": "4.17.13"
Expand Down
22 changes: 14 additions & 8 deletions packages/core/src/handlers/base-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export abstract class BaseHandler<TFile extends UploadxFile>
responseType: ResponseBodyType = 'json';
storage: BaseStorage<TFile>;
registeredHandlers = new Map<string, AsyncHandler>();
protected log = Logger.get(this.constructor.name);
logger: Logger;
protected _errorResponses = {} as ErrorResponses;

constructor(config: UploadxOptions<TFile> = {}) {
Expand All @@ -88,10 +88,10 @@ export abstract class BaseHandler<TFile extends UploadxFile>
if (config.userIdentifier) {
this.getUserId = config.userIdentifier;
}
this.logger = this.storage.logger;
this.assembleErrors();
this.compose();

this.log('options: %o', config);
this.logger.debug('Config:', config);
}

/**
Expand All @@ -115,7 +115,7 @@ export abstract class BaseHandler<TFile extends UploadxFile>
handler && this.registeredHandlers.set(method.toUpperCase(), handler);
// handler && this.cors.allowedMethods.push(method.toUpperCase());
});
this.log('Handlers', this.registeredHandlers);
this.logger.debug('Registered handlers: %s', [...this.registeredHandlers.keys()].join(', '));
}

assembleErrors(customErrors = {}): void {
Expand All @@ -130,9 +130,9 @@ export abstract class BaseHandler<TFile extends UploadxFile>
handle = (req: http.IncomingMessage, res: http.ServerResponse): void => this.upload(req, res);

upload = (req: http.IncomingMessage, res: http.ServerResponse, next?: () => void): void => {
req.on('error', err => this.log(`[request error]: %o`, err));
req.on('error', err => this.logger.error(`[request error]: %o`, err));
this.cors.preflight(req, res);
this.log(`[request]: %s`, req.method, req.url);
this.logger.debug(`[request]: %s %s`, req.method, req.url);
const handler = this.registeredHandlers.get(req.method as string);
if (!handler) {
return this.sendError(res, { uploadxErrorCode: ERRORS.METHOD_NOT_ALLOWED } as UploadxError);
Expand All @@ -145,7 +145,13 @@ export abstract class BaseHandler<TFile extends UploadxFile>
.call(this, req, res)
.then(async (file: TFile | UploadList): Promise<void> => {
if ('status' in file && file.status) {
this.log('[%s]: %s', file.status, file.name);
this.logger.debug(
'[%s]: %s: %d/%d',
file.status,
file.name,
file.bytesWritten,
file.size
);
this.listenerCount(file.status) &&
this.emit(file.status, { ...file, request: pick(req, ['headers', 'method', 'url']) });
if (file.status === 'completed') {
Expand All @@ -169,7 +175,7 @@ export abstract class BaseHandler<TFile extends UploadxFile>
]) as UploadxError;
const errorEvent = { ...err, request: pick(req, ['headers', 'method', 'url']) };
this.listenerCount('error') && this.emit('error', errorEvent);
this.log('[error]: %o', errorEvent);
this.logger.error('[error]: %o', errorEvent);
if ('aborted' in req && req['aborted']) return;
return this.sendError(res, error);
});
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/storages/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { logger, Logger } from '../utils';
import { File } from './file';
import { BaseStorageOptions } from './storage';

Expand All @@ -10,7 +11,8 @@ class ConfigHandler {
onComplete: () => null,
path: '/files',
validation: {},
maxMetadataSize: '4MB'
maxMetadataSize: '4MB',
logger: logger
};

private _config = this.set(ConfigHandler.defaults);
Expand All @@ -22,6 +24,10 @@ class ConfigHandler {
get<T extends File>(): Required<BaseStorageOptions<T>> {
return this._config as unknown as Required<BaseStorageOptions<T>>;
}

getLogger(): Logger {
return this._config.logger;
}
}

export const configHandler = new ConfigHandler();
3 changes: 1 addition & 2 deletions packages/core/src/storages/disk-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ export class DiskStorage extends BaseStorage<DiskFile> {
}
this.accessCheck().catch(err => {
this.isReady = false;
// eslint-disable-next-line no-console
console.error('ERROR: Could not write to directory: %o', err);
this.logger.error('[error]: Could not write to directory: %o', err);
});
}

Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/storages/local-meta-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ export class LocalMetaStorage<T extends File = File> extends MetaStorage<T> {
super(config);
this.directory = (config?.directory || join(tmpdir(), 'uploadx_meta')).replace(/\\/g, '/');
this.accessCheck().catch(err => {
// eslint-disable-next-line no-console
console.error('ERROR: Could not write to directory: %o', err);
this.logger.error('[error]: Could not write to directory: %o', err);
});
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/storages/meta-storage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FileName } from './file';
import { configHandler } from './config';

/** @experimental */
export interface UploadListEntry {
Expand Down Expand Up @@ -26,6 +27,7 @@ export interface MetaStorageOptions {
export class MetaStorage<T> {
prefix = '';
suffix = '';
logger = configHandler.getLogger();

constructor(config?: MetaStorageOptions) {
this.prefix = config?.prefix || '';
Expand Down
21 changes: 16 additions & 5 deletions packages/core/src/storages/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isEqual,
Locker,
Logger,
LogLevel,
toMilliseconds,
typeis,
Validation,
Expand Down Expand Up @@ -81,6 +82,13 @@ export interface BaseStorageOptions<T extends File> {
* ```
*/
expiration?: ExpirationOptions;
/** Custom logger injection */
logger?: Logger;
/**
* Set built-in logger severity level
* @defaultValue 'none'
*/
logLevel?: LogLevel;
}

const LOCK_TIMEOUT = 300; // seconds
Expand All @@ -96,7 +104,7 @@ export abstract class BaseStorage<TFile extends File> {
checksumTypes: string[] = [];
errorResponses = {} as ErrorResponses;
cache: Cache<TFile>;
protected log = Logger.get(`${this.constructor.name}`);
logger: Logger;
protected namingFunction: (file: TFile, req: any) => string;
protected validation = new Validator<TFile>();
abstract meta: MetaStorage<TFile>;
Expand All @@ -109,8 +117,11 @@ export abstract class BaseStorage<TFile extends File> {
this.maxUploadSize = bytes.parse(opts.maxUploadSize);
this.maxMetadataSize = bytes.parse(opts.maxMetadataSize);
this.cache = new Cache(1000, 300);

const purgeInterval = toMilliseconds(this.config.expiration?.purgeInterval);
this.logger = opts.logger;
if (opts.logLevel && 'logLevel' in this.logger) {
this.logger.logLevel = opts.logLevel;
}
const purgeInterval = toMilliseconds(opts.expiration?.purgeInterval);
if (purgeInterval) {
this.startAutoPurge(purgeInterval);
}
Expand Down Expand Up @@ -216,7 +227,7 @@ export abstract class BaseStorage<TFile extends File> {
const [deleted] = await this.delete({ id });
purged.items.push({ ...deleted, ...rest });
}
purged.items.length && this.log(`Purge: removed ${purged.items.length} uploads`);
purged.items.length && this.logger.info(`Purge: removed ${purged.items.length} uploads`);
}
return purged;
}
Expand Down Expand Up @@ -265,7 +276,7 @@ export abstract class BaseStorage<TFile extends File> {

protected startAutoPurge(purgeInterval: number): void {
if (purgeInterval >= 2147483647) throw Error('“purgeInterval” must be less than 2147483647 ms');
setInterval(() => void this.purge().catch(this.log), purgeInterval);
setInterval(() => void this.purge().catch(e => this.logger.error(e)), purgeInterval);
}

protected updateTimestamps(file: TFile): TFile {
Expand Down
81 changes: 76 additions & 5 deletions packages/core/src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,80 @@
import { debug } from 'debug';
import { formatWithOptions } from 'util';

type LoggerInstance = ((label?: any, ...data: any[]) => void) & { enabled?: boolean };
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
const levels = ['debug', 'info', 'warn', 'error', 'none'];

export class Logger {
static get(namespace: string): LoggerInstance {
return debug('uploadx').extend(namespace.toLowerCase());
enum PriorityOf {
debug,
info,
warn,
error,
none
}

export interface LoggerOptions {
logger?: Logger;
logLevel?: LogLevel;
label?: string;
write?: (data: unknown[], level?: LogLevel) => void;
}

export interface Logger {
logLevel?: LogLevel;
debug(...data: any[]): void;
info(...data: any[]): void;
warn(...data: any[]): void;
error(...data: any[]): void;
}

/**
* Basic logger implementation
*/
export class BasicLogger implements Logger {
label: string;
private readonly logger: Logger;
private _logLevel: LogLevel = 'none';

constructor(readonly options: LoggerOptions = {}) {
this.logger = options.logger || console;
this.label = options.label ?? 'uploadx:';
if (options.logLevel) this.logLevel = options.logLevel;
if (options.write) this.write = options.write;
}

get logLevel(): LogLevel {
return this._logLevel;
}

set logLevel(value: LogLevel) {
if (value && !levels.includes(value)) {
throw new Error(`Invalid log level: ${value}, supported levels are: ${levels.join(', ')}.`);
}
this._logLevel = value;
}

write = (data: unknown[], level: Exclude<LogLevel, 'none'>): void => {
if (PriorityOf[level] >= PriorityOf[this._logLevel]) {
const message = formatWithOptions({ colors: true, depth: 1, maxStringLength: 80 }, ...data);
const timestamp = new Date().toISOString();
this.logger[level](`${timestamp} ${level.toUpperCase()} ${this.label} ${message}`);
}
};

info(...data: unknown[]): void {
this.write(data, 'info');
}

warn(...data: unknown[]): void {
this.write(data, 'warn');
}

error(...data: unknown[]): void {
this.write(data, 'error');
}

debug(...data: unknown[]): void {
this.write(data, 'debug');
}
}

export const logger = new BasicLogger({});
Loading

0 comments on commit ce9d5a0

Please sign in to comment.