Logging levels for Swift's server-side logging APIs and new os_log APIs

Actually I am not very happy with the Alert level (see my comment above).

Alerting situations just do not sound worse than critical situation. (Non-native speaker)

(Plus: Trace is missing)

For command line tools, -v9 or -vvvv is quite common to enable more logging. Maybe there should be a way to set or modify the log level based on a number? The model is: The higher the log level the user configures, the more logs get printed (I cannot recall any tool that logs less if you configure a higher log level). But I don't think this requires the internal numbering schema to be public as long as there is a way to get the next more/less severe level (maybe with offset, so something like currentLevel = Logger.Level(5, levelsAbove:currentLevel) is possible.).

The Comparable intro says:

“The Comparable protocol is used for types that have an inherent order, such as numbers and strings.”

I think this is very true for log levels.

Level could also be named Severity (as it is in many logging frameworks), making the order obvious, too: Trace < Debug < Info < Notice < Warning < Error < Critical < Fatal < Panic.

I would prefer Comparable over isMoreSevere/isMoreSevereOrEqual/isLessSevere/isLessSevereOrEqual/isEquallySevere.


Renaming Level to Severity might actually be worth it …


If case you want to reduce the number of initial levels: I think Trace/Debug, Info/Notice or Notice/Warning, Error/Critical, Fatal/Panic could be merged. Debug, Info, Warning, Error, Fatal was usually enough for all kinds of systems (with Trace being nice to have). That's:

  • Users don't want to see (Debug)
  • Curious users want to see (Info)
  • Users want to see when they suspect a problem (Warning)
  • Users have to see (Error)
  • Users already noticed (Fatal)

With hidden internal numeric values, the first version could leave gaps that allow later addition of levels.

I don't mind keeping the numerical codes for serialisation purposes, but I totally agree that Comparable conformance should be removed.

I'm not sure that naming it Severity makes it any much better. I'd prefer to be explicit with isMoreSevere(than:).

Not having numerical codes sounds like a good idea to me. Also, it wouldn't affect os_log APIs as we weren't planning on supporting it in the new APIs.

1 Like

@dhoepfl, I'll only reply to my part of the proposed which is removing the numerical values. I personally don't mind what the log levels are at all, I just adopted what seemed like the outcome of this thread.

So there's nothing that precludes an application from assigning the various log levels an integer code that matches their expectation. You could totally do:

extension Logger.Level {
    var numericalValue: Int {
        switch self {
            [...]
            case .notice: return 2
            case .info: return 1
            case .debug: return 0
       }
    }
}

Exactly! We have seen in this thread that there is not one inherent order for log levels, there's at least two:

  1. what you (& I) think is intuitive ... < .debug < .info < ... < .warning < .error < ...
  2. what syslog does and the proposal authors recommend: ... < .error < .warning < ... < .info < .debug < ...

And because there's not one order is exactly why I propose to remove the numerical value from the logging library itself.

What do @ravikandhadai & @Ben_Cohen think about that?

If we don't remove the numerical values from the enum Level but just remove the Comparable you could still do Logger.Level.info.rawValue < Logger.Level.error.rawValue and expect true (but currently it'd be false). Do you think that's okay?

I also prefer Level with more explicit comparison functions but I don't feel super strongly about it.

one different way of “solving” this could be to keep the numbers but use the natural ordering rather than the syslog one, iow keep the syslog naming but use easier to reason about numeric values

1 Like

This sounds even more confusing so I think I'd rather -1 on this... Reasoning:

  • since we adopt syslog-like wording, someone who is familiar with it recognizes it and expects 0 == most severe.
    • the above proposal would break this intuition
  • for people not familiar with syslog severity severities, they have no assumptions about the numeric values, so any ordering should be fine really; they may guess at first but guessing at an API can backfire.
    • offering explicit isLess/MoreSevere(than: ...) solves this problem both for: newcomers and people who know the levels, but would make mistakes using the numeric values raw anyway.

So I think we should either hide, or keep syslog levels, and I'd be against using syslog level names, and opposite numbering.

1 Like

FWIW, most of the log level wording is not unique to syslog. All loggers use critical, debug, info, warning, and error (ie: Python or Log4J). The only part of the syslog levels that is unique are the notice, emergency, and alert levels.

I bet most people aren't going to read the docs to see which levels are available, they're going to read the docs about how to create a logger (or just find/follow some tutorial online) and then they'll reach for the natural level they need for what they're logging. 99% of logging is somewhere between info and critical and every logger I've ever used has the standard levels I need. I don't think I've ever had to look up which levels are available nor what their integer values are, unless I was creating my own custom logging levels (which was decided to not be available in this logging framework).


Perhaps not the best suggestion, but a suggestion nonetheless; We could hide the actual numerical value but still permit Comparable and document that it works based on the severity of the level where emergency is the most severe and debug is the least. Anyone who doesn't read the docs, but tests their code would quickly find out that the comparator does not use the standard syslog numerical values. I would prefer Comparable conformance over a function named isMoreSevere(than:).

How often are people actually going to be comparing log levels? Shouldn't people just be writing log.warn("Something bad might happen") and the handler will decide if the message needs to be logged based on the configured log level?

Sure, but we are talking from the perspective of libraries implementing loggers, not using them. E.g. a log handler which determines if it should log or not, based on some env variable or config value. Users don't care and do what feels right, as you correctly point out. It matters "more" for middleware library authors who may want to utilize notice, and it matters more more for implementations of log handlers. Though it all boils down to "give me a way to compare them", which is why I'm in favor of hiding numbers, and offering comparison methods.

Indeed, this is mostly a thing for the API and users won't care at all, thus keeping it flexible sounds good IMHO. Now that I thought about it, even LogHandlers most of the time should not really be comparing those, since this happens at Logger level already -- but they might. So this is only about logging API and its potential implementors.

2 Likes

I've opened a PR based on this request that might help steer further discussion: https://github.com/apple/swift-log/pull/33

1 Like

I think that’s okay because it’s fairly contrived: it’s less visible than a Comparable conformance.

1 Like

My bad, I must have skimmed past that part of the discussion.

I totally agree that this is really all that matters in the end. As long as the documentation is clear and the behavior isn't too wild then I think Comparable conformance is still valid, however we might implement it (though I personally prefer a severity-based comparison over a syslog numerical value comparison).

1 Like

I don't think I quite grasp how these are any fundamentally different.

// proposed
if level.isMoreSevere(than: self.logLevel) { ... }

// current, with different underlying semantic logic
if level >= self.logLevel { ... }

Both communicate the same, but with methods we would need to define the permutations of the comparisons that we could get with less code through Comparable.

When looking at the code, I read both as the same. If the level is higher or equal to the logLevel.

The difference is:

  • syslog thinks: .debug > .error
  • many people on this thread think: .error > .debug

but

  • .error.isMoreSevere(than: .debug) is clearly true
  • .debug.isMoreSevere(than: .error) is clearly false

So whilst there are two differing opinions about if the numerical value of .error or .debug should be larger, there don't seem to be differing opinions about whether .error or .debug is more severe.

1 Like

But syslog doesn't think .debug > .error, it thinks the numeric representation is.

That's where I'm getting lost, and could just be my lack of systems engineering experience is hindering me.

It could be that part of the issue is we're trying to have a two-axis value (severity & numeric value) through a single axis channel.

Maybe we just define:

extension Logger.Level {
    public var severity: Int {
        switch self {
        case .debug: return 0
        ...
        case .emergency: return 7
        }
    }
}

which leaves people to do this lower comparison: if log.severity >= logLevel.severity?

Though, I suppose that is no different than just defining Level: Int without Comparable...

Going the method route, how would this equate?

.error.isMoreSevere(than: .error) true or false?

Do we also add isMoreSevere(orEqualTo:)?

Ok, sorry, I get what you're saying now. Syslog defines them as

/*
 * priorities/facilities are encoded into a single 32-bit quantity, where the
 * bottom 3 bits are the priority (0-7) and the top 28 bits are the facility
 * (0-big number).  Both the priorities and the facilities map roughly
 * one-to-one to strings in the syslogd(8) source code.  This mapping is
 * included in this file.
 *
 * priorities (these are ordered)
 */
#define LOG_EMERG       0       /* system is unusable */
#define LOG_ALERT       1       /* action must be taken immediately */
#define LOG_CRIT        2       /* critical conditions */
#define LOG_ERR         3       /* error conditions */
#define LOG_WARNING     4       /* warning conditions */
#define LOG_NOTICE      5       /* normal but significant condition */
#define LOG_INFO        6       /* informational */
#define LOG_DEBUG       7       /* debug-level messages */

so LOG_ERR < LOG_DEBUG is true. But syslog calls them 'priorities' so LOG_ERR has higher priority than LOG_DEBUG which in UNIX-think maps to a lower number for LOG_ERR than LOG_DEBUG.

But I can kind of see you point now and I would be happy to remove the numerical values but keep the < and use it as .debug < .error == true. Let's see what @Ben_Cohen & @ravikandhadai think.

1 Like

So if I understand correctly, the choice is between:

  • Defining an logLevel.isMoreSevere function vs
  • Defining a severity: Int property on logLevel

If our goal is to only support comparisons between logging levels and nothing more, then I think the first option of defining an explicit function that does the comparison captures the intent and also allows just that. (The second option seems to allow lot more (possibly undesirable) behaviors .e.g .info.severity + 1 >= .warning.severity etc.)

Sort of - to summarize my understanding:

We want to leave the serialization of the (numeric representation) values of Logger.Level up to the implementor while still offering the ability to compare the semantic ordering of a Logger.Level relative to another.

But it's undecided if that should be done with Comparable conformance or with a method.

As mentioned earlier by others in this thread, I think a method with a descriptive name is better than an operator like < as something like .critical < .error doesn't convey in the APIs whether the ordering is increasing or decreasing with respect to severity.

1 Like