Project

General

Profile

Actions

Feature #20864

open

Allow `Kernel#warn` to accept `**options` and pass these to `Warning.warn`.

Added by ioquatix (Samuel Williams) about 1 month ago. Updated about 1 month ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:119726]

Description

Background

Structured logging is a practice that organizes log data in a consistent, easily parseable format. Unlike traditional unstructured logs, which often consist of plain text entries, structured logs format information in key-value pairs or structured data formats such as JSON. This allows for better automation, searchability, and analysis of log data.

In Ruby, Kernel#warn is extremely useful, especially because there is a mechanism for loggers to redirect warnings using the Warning module. However, it is difficult to generate structured logs with Kernel#warn as all the positional arguments are converted to a single string, and arbitrary keyword options are rejected.

As a consequence, code like this is not possible:

begin
  ...
rescue => error
  warn "Something went wrong!", exception: error
end

It is very desirable to have a standard interface in Ruby for emitting structured warnings.

Proposal

I'd like to extend the current implementation to allow all options to be forwarded to Warning.warn. This would allow us to add more details to warnings and emit structured logs using Warning.warn.

A simple example of the proposed interface:

module Kernel
  def warn(*arguments, uplevel: ..., **options)
    # Existing processing of arguments -> message
    ::Warning.warn(message, **options)
  end
end

Current behaviour rejects any unknown options:

warn("Oops", exception: error)
# => <internal:warning>:50:in `warn': unknown keyword: :exception (ArgumentError)

I don't have an opinion about the implementation, but I wanted to get feedback on the interface.

Regarding the default behaviour, I propose no changes.

Updated by zverok (Victor Shepelev) about 1 month ago

I wonder why error: in particular, and not just warn(text, **any_structured_metadata)?..

Updated by Eregon (Benoit Daloze) about 1 month ago

Why not include the error directly in the warning message?
It seems that would just work better with the whole Warning.warn system and existing handling of warnings.

Updated by ioquatix (Samuel Williams) about 1 month ago ยท Edited

I wonder why error: in particular, and not just warn(text, **any_structured_metadata)?..

I'd be okay with that, in fact I was thinking of making that proposal but I wasn't sure it would be accepted if it was too general.

Why not include the error directly in the warning message?

Good question! Unfortunately, I found that the formatting behaviour of Warning.warn is a bit problematic. The first argument to Warning.warn is a string concatenation of all arguments passed to Kernel#warn. Depending on the log output, I want to include the details of the error in a JSON format for structured logging. In order to do that, I need the original objects passed to warn. This includes adding trace metadata which may not be known about until later on in the output handling.

One option would be to do the formatting at the point of the Kernel#warn call. However, this implies that we know what formatting is desired at that point (we don't), and greatly complicates the usage of the interface.

It may be the case that warn was not designed to be used that way, and I'm okay if that's the case. But I think this is a useful and generic extension point for logging. If we can allow extra meta-data (that can be ignored by default) to go via **options, it significantly increases the usefulness of this interface - we can pick up those options in Warning.warn and use those to control the output accordingly.

Updated by Dan0042 (Daniel DeLorme) about 1 month ago

I think I agree with the general idea but I'm not sure if the proposal is what I think it is. It's a bit thin about what happens with this "error" keyword exactly. So is the idea that this meta-information would be forwarded to another method to decide what to do with it? Are we talking about extending Warning.warn to work like: warn(*args, **opts) -> Warning.warn(formatted_str, **opts)

Updated by ioquatix (Samuel Williams) about 1 month ago

Thanks for your feedback. The proposal is deliberately thin - as I said, I don't know exactly how we should adjust the internal interface to handle this (actually I don't have a strong opinion about it). The main point is, to allow for an additional keyword argument. I think Warning.warn should probably receive that keyword argument, and maybe print it using error.full_message or something similar. I don't think it should be part of formatted_str but I think the default implementation could print it separately?

Actions #6

Updated by ioquatix (Samuel Williams) about 1 month ago

  • Description updated (diff)
Actions #7

Updated by ioquatix (Samuel Williams) about 1 month ago

  • Subject changed from Support `error:` keyword to `Kernel#warn` to Allow `Kernel#warn` to accept `**options` and pass these to `Warning.warn`.
  • Description updated (diff)

Updated by ioquatix (Samuel Williams) about 1 month ago

According to the discussion I've updated the proposal.

Actions #9

Updated by ioquatix (Samuel Williams) about 1 month ago

  • Description updated (diff)

Updated by jeremyevans0 (Jeremy Evans) about 1 month ago

@ioquatix (Samuel Williams) asked for my feedback on this. I'm against Warning.warn taking **opts for structured logging. We have an existing category keyword option that it would conflict with, and it would prevent the backwards compatible addition of other keyword arguments. If we want to support structured logging in Warning.warn, we should have a keyword argument specifically for the structured logging information. We should also have a description of how the structured logging information should be handled by default.

Updated by ioquatix (Samuel Williams) about 1 month ago

  • Description updated (diff)

We should also have a description of how the structured logging information should be handled by default.

Regarding the default behaviour, I propose no changes.

As an alternative, I'd also be open to dumping it, e.g. using pp.

We have an existing category keyword option that it would conflict with, and it would prevent the backwards compatible addition of other keyword arguments.

I don't completely agree with the former part of this statement, but the latter part is fair. It's a toss up for usability and whether we think that we will add more keywords in the future.

e.g.

# Jeremy's preference (some top level keyword argument):
warn "something went wrong", extra: {exception:}

# vs the current proposal:
warn "something went wrong", exception:

I slightly lean towards the usability aspect for the consumer of the API, but I understand Jeremy's concern regarding the long term compatibility of the internal interface Warning.warn.

Updated by ioquatix (Samuel Williams) about 1 month ago

I checked C interface in error.h and we end up having a lot of variants:

  • rb_warn
  • rb_warning
  • rb_category_warn
  • rb_compile_warn
  • rb_category_compile_warn

etc.

One idea is this:

struct rb_warning_arguments {
  rb_warning_category_t category;
  VALUE extra;
  // others
};

void rb_warning(struct rb_warning_arguments, const char *fmt, ...);

For things like:

void rb_category_compile_warn(rb_warning_category_t cat, const char *file, int line, const char *fmt, ...);

I imagine that cat, file and line are all useful structured data.

Updated by ko1 (Koichi Sasada) about 1 month ago

Compatibility issues for user defined warn methods?

$ ~/gem-codesearch/gem-codesearch 'def warn\b' | wc -l
3837

Updated by ioquatix (Samuel Williams) about 1 month ago

For previous discussion including compatibility, see https://bugs.ruby-lang.org/issues/17122

Updated by shyouhei (Shyouhei Urabe) about 1 month ago

I also want something like this. In order to move things forward we have to clear some points:

  • There could be programs that redefine Kernel#warn on the fly in the wild. They might suffer from the API change. About the only thing we can do here is let them die miserably. Is it an acceptable choice? (I guess so though)

  • The other issue is Warning.warn side. We already have it, which also doesn't expect unknown arguments for now. We acknowledge that Warning.warn could take exactly one argument (per https://bugs.ruby-lang.org/issues/17122#note-15). We are checking if it takes category: every time now. If we were to pass arbitrary keyword arguments to Warning.warn is the check... kind of possible in practice?

These two might be related, but are not the same thing.

Updated by Eregon (Benoit Daloze) about 1 month ago

From the experience of mspec or ruby/spec which used to override Kernel#warn it's very confusing and messy, and I removed that.
Overriding Kernel#warn typically also breaks uplevel handling and might also break the recursion check (some people user super in Warning.warn).
So I think one should never override Kernel#warn, especially since we have Warning.warn now as the designated extension point.

I wonder if we really need structured warnings.
Warnings at least currently seem primarily meant for developers, and I don't really see the value for structured warnings there.
So I think it would be good to have a concrete example where this would be useful and it's not trivially worked around.

I would think if we want structured something it would make more sense for logging and for exceptions.
Warnings are not logging.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0