Skip to content

Commit

Permalink
fix: schema being required
Browse files Browse the repository at this point in the history
  • Loading branch information
ImLunaHey committed Jan 1, 2024
1 parent ccdc360 commit c8e4e65
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 41 deletions.
50 changes: 25 additions & 25 deletions src/examples/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,47 @@ import z from 'zod';
import { BaseSchema, Logger } from '../logger';

const schema = {
debug: {
'stats': z.object({
a: z.number(),
}),
},
error: {
'error': z.object({}),
},
info: {},
warn: {},
debug: {
stats: z.object({
a: z.number(),
}),
},
error: {
error: z.object({}),
},
info: {},
warn: {},
} satisfies BaseSchema;

const logger = new Logger({
service: 'my-service',
schema,
service: 'my-service',
schema,
});

logger.debug('stats', {
a: 123,
a: 123,
});

// This will only log the "a" field
logger.debug('stats', {
a: 123,
/**
* This will show the following typescript error
*
* Argument of type '{ a: number; b: number; }' is not assignable to parameter of type '{ a: number; }'.
* Object literal may only specify known properties, and 'b' does not exist in type '{ a: number; }'.
*/
b: 123123,
a: 123,
/**
* This will show the following typescript error
*
* Argument of type '{ a: number; b: number; }' is not assignable to parameter of type '{ a: number; }'.
* Object literal may only specify known properties, and 'b' does not exist in type '{ a: number; }'.
*/
b: 123123,
});

logger.debug('stats', {
a: 1
a: 1,
});

// `error` is required when using `.error` method
// `cause` is optional
logger.error('error', {
error: new Error('A thing happened', {
cause: new Error('This thing caused it'),
})
error: new Error('A thing happened', {
cause: new Error('This thing caused it'),
}),
});
60 changes: 44 additions & 16 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,19 @@ export type BaseSchema = {
[level in LogLevel]?: Record<string, z.ZodObject<any, any, any>>;
};

type Options<Schema extends BaseSchema> = {
type Options<Schema extends BaseSchema = BaseSchema> = {
service: string;
schema?: Schema;
defaultMeta?: Record<string, unknown>;
};

type MetaForSchema<Schema extends BaseSchema, Level extends keyof Schema, Message> = Message extends keyof Schema[Level]
const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
type Literal = z.infer<typeof literalSchema>;
type Json = Literal | { [key: string]: Json } | Json[];

type MetaForSchema<Schema extends BaseSchema, Level extends keyof Schema, Message> = [Message] extends [never]
? z.input<z.ZodType<Json>>
: Message extends keyof Schema[Level]
? Schema[Level][Message] extends z.ZodType<any, any, any>
? z.input<Schema[Level][Message]>
: undefined
Expand Down Expand Up @@ -173,11 +179,17 @@ export class Logger<Schema extends BaseSchema> {
this.schema = options.schema;
}

private log<Message extends keyof Schema[LogLevel]>(
level: LogLevel,
private log<Level extends LogLevel, Message extends keyof Schema[Level], Data extends Schema[Level][Message]>(
level: Level,
message: Message,
data: Schema[LogLevel][Message] extends z.ZodType ? z.input<Schema[LogLevel][Message]> : undefined,
data: Data extends z.ZodType ? z.input<Data> : z.input<any>,
) {
// If we don't have a schema just log the message
if (!this.schema) {
this.logger[level](message as string, data);
return;
}

// Ensure meta is valid before logging
const parser = this.schema?.[level]?.[message as string];
const parsedData = parser?.safeParse(data);
Expand All @@ -194,26 +206,42 @@ export class Logger<Schema extends BaseSchema> {
this.logger[level](message as string, meta);
}

debug<Message extends keyof Schema['debug']>(message: Message, meta: DebugMeta<Schema, Message>) {
this.log('debug', message, meta);
debug<Message extends keyof Schema['debug']>(
message: [Message] extends [never] ? string : Message,
meta: DebugMeta<Schema, Message>,
) {
this.log('debug', message as Message, meta as any);
}

info<Message extends keyof Schema['info']>(message: Message, meta: InfoMeta<Schema, Message>) {
this.log('info', message, meta);
info<Message extends keyof Schema['info']>(
message: [Message] extends [never] ? string : Message,
meta: InfoMeta<Schema, Message>,
) {
this.log('info', message as Message, meta as any);
}

warn<Message extends keyof Schema['warn']>(message: Message, meta: WarnMeta<Schema, Message>) {
this.log('warn', message, meta);
warn<Message extends keyof Schema['warn']>(
message: [Message] extends [never] ? string : Message,
meta: WarnMeta<Schema, Message>,
) {
this.log('warn', message as Message, meta as any);
}

error<Message extends keyof Schema['error']>(message: Message, meta: ErrorMeta<Schema, Message>) {
error<Message extends keyof Schema['error']>(
message: [Message] extends [never] ? string : Message,
meta: ErrorMeta<Schema, Message>,
) {
// If the error isn't an error object make it so
// This is to prevent issues where something other than an Error is thrown
// When passing this to transports like Axiom it really needs to be a real Error class
if (meta?.error && !(meta?.error instanceof Error)) meta.error = new Error(`Unknown Error: ${String(meta.error)}`);
this.log('error', message, {
...meta,
error: serialiseError(meta?.error as Error),
});
this.log(
'error',
message as Message,
{
...meta,
error: serialiseError(meta?.error as Error),
} as any,
);
}
}

0 comments on commit c8e4e65

Please sign in to comment.