-
Notifications
You must be signed in to change notification settings - Fork 936
Enhance logger function typing #2230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| info('The json is %j', { a: 1, b: '2' }); | ||
| info('The object is %O', { a: 1, b: '2' }); | ||
| info('The answer is %d and the question is %s with %o', 42, 'unknown', { correct: 'order' }); | ||
| info('Missing placeholder is fine %s'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've allowed strings with missing placeholder data but if we want to be more strict and require that they add them we can just change the type of args from ...args: ParseLogFnArgs<TMsg> | [] to ...args: ParseLogFnArgs<TMsg> .
But, this is technically valid
|
lgtm 🚀 |
mcollina
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wow. looks really good!
|
In case you want something for a changelog, we wrote something like this for ours. Feel free to throw it away/tweak it however you would like @mcollina 🙏 '@seek/logger': majorEnforce stricter typing for logger methods This is a breaking change to the types, improving type safety by enforcing stricter parameter typing on all logger methods. Existing code that passes metadata after the message string will need to be updated. This pattern was previously silently ignoring the metadata, but now triggers a type error to prevent data loss. Before (no longer works): logger.error('my message', { err, metadata });After (correct usage): logger.error({ err, metadata }, 'my message');This ensures all metadata is properly captured and logged. This update also enforces type checking for placeholder values in message strings: Before (no longer works): logger.error({ err, metadata }, 'my message error code: %d', 'Bad Request');After (correct usage): logger.error({ err, metadata }, 'my message error code: %d', 400); |
|
One small side-effect of this which someone in my team ran into is if you're trying to access the Eg. logger.warn.mock.calls[0]?.[1]You may need to change that to: logger.warn.mock.calls[0]?.at(1) |
|
@samchungy we noticed an oddity -- we're using a The hook allowed us to write code like this: // ...
} catch (err) {
pino.error(getContext(), 'Failed to send error to Slack:', err);
}And also this: console.info = (...args) => {
pino.info(getContext(), ...args);
};After this change to Pino, it seems that now we have to define a type that represents the prior definition of logFn and use it like this: type UnsafePinoLogFn = (obj: unknown, msg?: string, ...args: unknown[]) => void;
// ...
} catch (err) {
(pino.error as unknown as UnsafePinoLogFn)(
getContext(),
'Failed to send error to Slack:',
err
);
// ...
console.info = (...args) => {
(pino.info as unknown as UnsafePinoLogFn)(getContext(), ...args);
};
// ... etcDoes that seem to make sense? |
|
Is This change is bound to get people coming here, but unless you're using a placeholder string, Pino actually drops everything after a message string. I would suggest something like: pino.error({ ...getContext(), err }, 'Failed to send error to Slack:'); |
|
Oh, So, maybe this is unorthodox, but we're monkeypatching /**
* A custom logMethod hook for Pino that mimics console.log behavior
* for handling multiple arguments beyond the first input.
*/
const consoleLogPinoHook = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
logMethod(args: any[], method: (...args: any[]) => void) {
if (args.length <= 2) {
// Default behavior for a single argument
method.apply(this, args);
return;
}
// Handle multiple arguments like console.log does
// Format: first argument is the message (potentially with format specifiers)
// followed by any number of arguments to be substituted or concatenated
const contextArg = args.shift();
const firstArg = args[0];
const restArgs = args.slice(1);
// If the first argument is a string and contains format specifiers,
// handle them similar to how console.log would
{ ... }
// Else no format specifiers or first arg is not a string
// Just concatenate everything with spaces, like console.log does
{ ... }
},
}; |
|
The change I made where was to help prevent users from sending their metadata to the void for a logger with standard configuration. I think defining something custom would be inevitable for your case You can restore the previous types with an overload/declaration merging Eg. declare module "pino" {
interface LogFn {
<T extends object>(obj: T, msg?: string, ...args: any[]): void;
}
}I would keep this next to your monkey patching code. |
|
That makes sense! Thank you! |
|
I am also facing type error after recent update. The below code used to work earlier. class Logger {
private readonly requestInfo: TRequestInfo;
constructor(reqInfo: TRequestInfo) {
this.requestInfo = reqInfo;
}
public debug(msg: string, ...args: any[]): void {
pinoLogger.debug(this.requestInfo, msg, ...args);
}
}If I change the new type definition to have interface LogFn {
<T, TMsg extends string = string>(obj: T, msg?: T extends string ? never: TMsg, ...args:any]): void;
<_, TMsg extends string = string>(msg: TMsg, ...args: ParseLogFnArgs<TMsg> | []): void;
} |
Before you consider changing the types, are you also similarly patching Pino? Have you read why the change is happening? Are you aware args aren't being logged unless msg is a placeholder string? |
|
@Dhruv-Garg79 if I'm understanding your need correctly, you could make the This allows you to have type safety in your custom class. |
No
yes, but it will lead to lot of changes for us, that's why I asked if there was any other better way to handle it instead of just monkey patching types.
yes. and that doesn't affect us. since we are using a small wrapper over pino. Hey @AaronMoat, that's a good idea, but that will require us to make a lot of changes wherever we are using the logger. Because value can be T or undefined where %s or %j is used in our case. |
|
Hi @Dhruv-Garg79 — are you able to show a problematic TS Playground example? I'm not following the problem, nor what T is in your comment. |
|
@AaronMoat I have updated your TS playground with the issue. The problem arises when the templated value is null or undefined. |
|
I mean I think that makes sense there, you're trying to pass something that could be You can always bypass it with some casts or coercion. Either: l.debug('hello %s', value ?? '');
l.debug('hello %s', value as string);Or using eg. public debug<TMsg extends string = string>(
msg: TMsg,
...args: any
): void {
pinoLogger.debug(this.requestInfo, msg, ...args);
}Or declare the override as suggested above. |
|
At the risk of asking a stupid question: Why is this done as part of a minor release when it clearly breaks the public API (even if it's "only" the types) of a lot of functions on the main/only object? And if we say this is acceptable, shouldn't the changelog at least mark it as a breaking change (instead, it's even called "enhance", which suggests an improvement)? |
|
@samchungy I get it, but since it's logging. I think we should not need to handle it by casting or coercion. For now, I can override the types, but it should allow null/undefined directly. |
We can agree to disagree but I think if your template is expecting a string, the type should show that. Unless you were to introduce alternate identifiers eg. %ns (nullish string?), I think the types should reflect what the templates identifiers represent. How would you coerce null or undefined into %d which is expecting a number? That being said, go with |
yes we can. It should be use case dependent, and the thing we are talking about is logging right now. All types, according to me should allow null/undefined by default.
Types like %ns, %nd, %nj are a good middle ground for nullish types, which allow both null and undefined values. I have changes my types to be like this for now Also shouldn't we have a separate type for boolean as well? something like %b? |
I think TypeScript largely disagrees with you 🤷
This is open source, so PR away my friend. |
You took my statement out of context. I said for logging, of course, not for everything. What I wrote was - yes we can. It should be use case dependent, and the thing we are talking about is logging right now. All types, according to me should allow null/undefined by default.
Sure. |
It is not only the types, this is a super breaking change that disagrees with everything stated in the docs: https://getpino.io/#/docs/api?id=logging-method-parameters This is pretty common usage in our code base: It is used as per docs, but after this PR it is "wrong" according to types (but it works in runtime) How was this approved and merged? I don't even... 🤷 |
Well the metadata there doesn't get logged 🤷♂️ (have you checked your logs?) I'm not disagreeing that it should be a breaking change |
|
Then at the docs needs an urgent update. But it would have been great if this was a v10.0.0 release for everyone's sanity, it's kind of hard to patch the docs now. Usually it is preferred to have a version selector for the docs, now they change in the middle of v9. But it seems this change is legit then and that I might go through with the upgrade. Wishing there was a codemod for this :) |
I didn't write the docs but where does it say that's valid? |
|
Ouf.... I guess I can't read - apologies. You are right. The docs seem to agree more with this PR and you. 🙇 |
No probs! I knew this was gonna irk some people so I was hoping it would be a major release but I promise it was done purely out of good intentions 🙏 |
An even bigger upgrade to #1995
This is a breaking change to the types, improving type safety by enforcing stricter parameter typing on all logger methods.
Existing code that passes metadata after the message string will need to be updated. This pattern was previously silently ignoring the metadata, but now triggers a type error to prevent data loss.
Before (no longer works):
After (correct usage):
This ensures all metadata is properly captured and logged.
This update also enforces type checking for placeholder values in message strings:
Before (no longer works):
After (correct usage):