[Feedback] Server Logging API (with revisions)

Proposal Review: SSWG-0001 (Server Logging API)

After the great discussion thread, we are proposing this as a final revision of this proposal and enter the proposal review phase which will run until the 20th January 2019.

We have integrated most of the feedback from the discussion thread so even if you have read the previous version, you will find some changes that you hopefully agree with. To highlight a few of the major changes:

  • Logging metadata is now structures and now supports nested dictionaries and list values, in addition to strings of course.
  • only locally relevant metadata can now be passed to the individual log methods.
  • ship a multiplex logging solution with the API package

The feedback model will be very similar to the one known from Swift Evolution. The community is asked to provide feedback in the way outlined below and after the review period finishes, the SSWG will -- based on the community feedback -- decide whether to promote the proposal to the Sandbox maturity level or not.

What goes into a review of a proposal?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the evolution of the server-side Swift ecosystem.

When reviewing a proposal, here are some questions to consider:

  • What is your evaluation of the proposal?

  • Is the problem being addressed significant enough?

  • Does this proposal fit well with the feel and direction of Swift on Server?

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Thank you for contributing to the Swift Server Work Group!

What happens if the proposal gets accepted?

If this proposal gets accepted, the official repository will be created and the code (minus examples, the proposal text, etc) will be submitted. The repository will then become usable as a SwiftPM package and a version (likely 0.1.0) will be tagged. The development (in form of pull requests) will continue as a regular open-source project.


Proposal Review: SSWG-0001 (Server Logging API)

(EDIT: @johannesweiss : 2019-01-14: fixed typos/small errors/removed broken links, thanks @anandabits, @royhsu & @ktoso)

After the great discussion thread, we are proposing this as a final revision of this proposal and enter the proposal review phase which will run until the 20th January 2019.

We have integrated most of the feedback from the discussion thread so even if you have read the previous version, you will find some changes that you hopefully agree with. To highlight a few of the major changes:

  • Logging metadata is now structures and now supports nested dictionaries and list values, in addition to strings of course.
  • only locally relevant metadata can now be passed to the individual log methods.
  • ship a multiplex logging solution with the API package

The feedback model will be very similar to the one known from Swift Evolution. The community is asked to provide feedback in the way outlined below and after the review period finishes, the SSWG will -- based on the community feedback -- decide whether to promote the proposal to the Sandbox maturity level or not.

What goes into a review of a proposal?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the evolution of the server-side Swift ecosystem.

When reviewing a proposal, here are some questions to consider:

  • What is your evaluation of the proposal?

  • Is the problem being addressed significant enough?

  • Does this proposal fit well with the feel and direction of Swift on Server?

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Thank you for contributing to the Swift Server Work Group!

What happens if the proposal gets accepted?

If this proposal gets accepted, the official repository will be created and the code (minus examples, the proposal text, etc) will be submitted. The repository will then become usable as a SwiftPM package and a version (likely 0.1.0) will be tagged. The development (in form of pull requests) will continue as a regular open-source project.


Server Logging API

  • Proposal: SSWG-0001
  • Authors: Johannes Weiss & Tomer Doron
  • Preferred initial maturity level: Sandbox
  • Name: swift-server-logging-api
  • Sponsor: Apple
  • Status: Active review (9th...20th January, 2019)
  • Implementation: https://github.com/weissi/swift-server-logging-api-proposal, if accepted, a fresh repository will be created under Apple · GitHub
  • External dependencies: none
  • License: if accepted, it will be released under the Apache 2 license
  • Pitch: Server: Pitches/Logging
  • Description: A flexible API package that aims to become the standard logging API which Swift packages can use to log. The formatting and delivery/persistence of the log messages is handled by other packages and configurable by the individual applications without requiring users of the API package to change.

Introduction

Almost all production server software needs logging that works with a variety of packages. So far, there have been a number of different ecosystems (e.g. Vapor, Kitura, Perfect, ...) that came up with their own solutions for logging, tracing, metrics, etc.

The SSWG however aims to provide a number of packages that can be shared across within the whole Swift on Server ecosystem so we need some amount of standardisation. Because different applications have different requirements on what logging should exactly do, we are proposing to establish a server-side Swift logging API that can be implemented by various logging backends (called LogHandler).
LogHandlers are responsible to format the log messages and to deliver/persist them. The delivery might simply go to stdout, might be saved to disk or a database, or might be sent off to another machine to aggregate logs for multiple services. The implementation of concrete LogHandlers is out of scope of this proposal and also doesn't need to be standardised across all applications. What matters is a standard API libraries can use without needing to know where the log will end up eventually.

Motivation

As outlined above we should standardise on an API that if well adopted and applications should allow users to mix and match libraries from different vendors while still maintaining a consistent logging.
The aim is to support all widely used logging models such as:

  • one global logger, ie. one application-wise global that's always accessible
  • a scoped logger for example one per class/sub-system
  • a local logger that is always explicitly passed around where the logger itself can be a value type

There are also a number of features that most agreed we will need to support, most importantly:

  • log levels
  • attaching structured metadata (such as a request ID) to the logger and individual log messages
  • being able to 'make up a logger out of thin air'; because we don't have one true way of dependency injection and it's better to log in a slightly different configuration than just reverting to print(...)

On top of the hard requirements the aim is to make the logging calls as fast as possible if nothing will be logged. If a log message is emitted, it will be mostly up to the LogHandlers to do so in a fast enough way. How fast 'fast enough' is depends on the requirements, some will optimise for never losing a log message and others might optimise for the lowest possible latency even if that might not guarantee log message delivery.

Proposed solution

Overall, we propose three different core, top-level types:

  1. LogHandler, a protocol which defines the interface a logging backend needs to implement in order to be compatible.
  2. Logging, the logging system itself, used to configure the LogHandler to be used as well as to retrieve a Logger.
  3. Logger, the core type which is used to log messages.

To get started with this API, a user only needs to become familiar with the Logger type which has a few straightforward methods that can be used to log messages as a certain log level, for example logger.info("Hello world!"). There's one step a user needs to perform before being able to log: The have to have a Logger instance. This instance can be either retrieved from the logging system itself using

let logger = Logging.make("my-app")

or through some different way provided by the framework they use or a more specialised logger package which might offer a global instance.

The Logger

We propose one struct Logger which has a supports a number of different methods to possibly emit a log message. Namely trace, debug, info, warning, and error. To send a log a message you pick a log level and invoke the Logger method with the same name, for example

logger.error("Houston, we've had a problem")

We already briefly touched on this: Where do we get logger: Logger from? This question has two answers: Either the environment (could be a global variable, could be a function parameter, could be a class property, ...) provides logger or if not, it is always possible to obtain a logger from the logging system itself:

let logger = Logging.make("com.example.app")
logger.warning("uh oh, this is unexpected")

To get the best logging experience and performance, it is advisable to use .make to pass Loggers around or store them in a global/instance variable rather than .makeing new Loggers whenever one needs to log a message.

Detailed design

The full API of Logger is visible below except for one detail that is removed for readability: All the logging methods have parameters with default values indicating the file, function, and line of the log message.

public struct Logger {
    public var logLevel: Logging.Level

    public func log(level: Logging.Level, message: @autoclosure () -> String, metadata: @autoclosure () -> Logging.Metadata? = nil, error: Error? = nil)

    public func trace(_ message: @autoclosure () -> String, metadata: @autoclosure () -> Logging.Metadata? = nil)
    
    public func debug(_ message: @autoclosure () -> String, metadata: @autoclosure () -> Logging.Metadata? = nil)

    public func info(_ message: @autoclosure () -> String, metadata: @autoclosure () -> Logging.Metadata? = nil)
    
    public func warning(_ message: @autoclosure () -> String, metadata: @autoclosure () -> Logging.Metadata? = nil, error: Error? = nil)
    
    public func error(_ message: @autoclosure () -> String, metadata: @autoclosure () -> Logging.Metadata? = nil, error: Error? = nil)
    
    public subscript(metadataKey metadataKey: String) -> Logging.Metadata.Value? { get set }
    
    public var metadata: Logging.Metadata? { get set }
}

The logLevel property as well as the trace, debug, info, warning, and error methods are probably self-explanatory. But the information that can be passed alongside a log message deserves some special mentions:

  • message, a String that is the log message itself and the only required argument.
  • metadata, a dictionary of metadata information attached to only this log message. Please see below for a more detailed discussion of logging metadata.
  • error, an optional Error that can be sent along with warning and error messages indicating a possible error that led to this failure.

Both, message and metadata are @autoclosure parameters which means that if the message does not end up being logged (because it's below the currently configured log level) no unnecessary processing is done rendering the String or creating the metadata information.

Instead of picking one of the trace, debug, info, etc methods it is also possible to use the log method passing the desired log level as a parameter.

Logging metadata

In production environments that are under heavy load it's often a great help (and many would say required) that certain metadata can be attached to every log message. Instead of seeing

info: user 'Taylor' logged in
info: user 'Swift' logged in
warn: could not establish database connection: no route to host

where it might be unclear if the warning message belong to the session with user 'Taylor' or user 'Swift' or maybe to none of the above, the following would be much clearer:

info: user 'Taylor' logged in [request_UUID: 9D315532-FA5C-4E11-88E9-520C877F58B5]
info: user 'Swift' logged in [request_UUID: 35CC2687-CD1E-45A3-80B7-CCCE278797E6]
warn: could not establish database connection: no route to host [request_UUID: 9D315532-FA5C-4E11-88E9-520C877F58B5]

now it's fairly straightforward to identify that the database issue was in the request where we were dealing with user 'Taylor' because the request ID matches. The question is: How can we decorate all our log messages with the 'request UUID' or other information that we may need to correlate the messages? The easy option is:

log.info("user \(userID) logged in [request_UUID: \(currentRequestUUID)]")

and similarly for all log messages. But it quickly becomes tedious appending [request_UUID: \(currentRequestUUID)] to every single log message. The other option is this:

logger[metadataKey: "request_UUID"] = currentRequestUUID

and from then on a simple

logger.info("user \(userID) logged in")

is enough because the logger has been decorated with the request UUID already and from now on carries this information around.

In other cases however, it might be useful to attach some metadata to only one message and that can be achieved by using the metadata parameter from above:

logger.trace("user \(userID) logged in",
             metadata: ["extra_user_info": [ "favourite_colour": userFaveColour,
                                             "auth_method": "password" ]])

The above invocation will log the message alongside metadata merged from the logger's metadata and the metadata provided with the logger.trace call.

The logging metadata is of type typealias Logging.Metadata = [String: Metadata.Value] where Metadata.Value is the following enum allowing nested structures rather than just Strings as values.

public enum MetadataValue {
    case string(String)
    indirect case dictionary(Metadata)
    indirect case array([Metadata.Value])
}

Users usually don't need to interact with the Metadata.Value type directly as it conforms to the ExpressibleByStringLiteral, ExpressibleByStringInterpolation, ExpressibleByDictionaryLiteral, and ExpressibleByArrayLiteral protocols and can therefore be constructed using the string, array, and dictionary literals.

Examples:

logger.info("you can attach strings, lists, and nested dictionaries",
            metadata: ["key_1": "and a string value",
                       "key_2": ["and", "a", "list", "value"],
                       "key_3": ["where": ["we": ["pretend", ["it": ["is", "all", "Objective C", "again"]]]]]])
logger[metadataKey: "keys-are-strings"] = ["but", "values", ["are": "more"]]
logger.warning("ok, we've seen enough now.")

Custom LogHandlers

Just like metadata, custom LogHandlers are an advanced feature and users will typically just choose a pre-existing package that formats and persists/delivers the log messages in an appropriate way. Said that, the proposed package here is an API package and it will become much more useful if the community create a number of useful LogHandlers to say format the log messages with colour or ship them to Splunk/ELK.

We have already seen before that Logging.make is what gives us a fresh logger but that raises the question what kind of logging backend will I actually get when calling Logging.make? The answer: It's configurable per application. The application -- likely in its main function -- sets up the logging backend it wishes the whole application to use. Libraries should never change the logging implementation as that should owned by the application. Setting up the LogHandler to be used is straightforward:

Logging.bootstrap(MyFavouriteLoggingImplementation.init)

This instructs the Logging system to install MyFavouriteLoggingImplementation as the LogHandler to use. This should only be done once at the start of day and is usually left alone thereafter.

Next, we should discuss how one would implement MyFavouriteLoggingImplementation. It's enough to conform to the following protocol:

public protocol LogHandler {
    func log(level: Logging.Level, message: String, metadata: Logging.Metadata?, error: Error?, file: StaticString, function: StaticString, line: UInt)

    subscript(metadataKey _: String) -> Logging.Metadata.Value? { get set }

    var metadata: Logging.Metadata { get set }

    var logLevel: Logging.Level { get set }
}

The implementation of the log function itself is rather straightforward: If log is invoked, Logger itself already decided that given the current logLevel, message should be logged. In other words, LogHandler does not even need to compare level to the currently configured level. That makes the shortest possible LogHandler implementation really quite short:

public struct ShortestPossibleLogHandler: LogHandler {
    public var logLevel: Logging.Level = .info
    public var metadata: Logging.Metadata = [:]
    
    public init(_ id: String) {}
    
    public func log(level: Logging.Level, message: String, metadata: Logging.Metadata?, error: Error?, file: StaticString, function: StaticString, line: UInt) {
        print(message) // ignores all metadata, not recommended
    }
    
    public subscript(metadataKey key: String) -> Metadata.Value? {
        get { return self.metadata[key] }
        set { self.metadata[key] = newValue }
    }
}

which can be installed using

Logging.bootstrap(ShortestPossibleLogHandler.init)

Supported logging models

This API intends to support a number programming models:

  1. Explicit logger passing, ie. handing loggers around as value-typed variables explicitly to everywhere log messages get emitted from.
  2. One global logger, ie. having one global that is the logger.
  3. One logger per sub-system, ie. having a separate logger per sub-system which might be a file, a class, a module, etc.

Because there are fundamental differences with those models it is not mandated whether the LogHandler holds the logging configuration (log level and metadata) as a value or as a reference. Both systems make sense and it depends on the architecture of the application and the requirements to decide what is more appropriate.

Certain systems will also want to store the logging metadata in a thread/queue-local variable, some may even want try to automatically forward the metadata across thread switches together with the control flow. In the Java-world this model is called MDC, your mileage in Swift may vary and again hugely depends on the architecture of the system.

I believe designing a MDC (mapped diagnostic context) solution is out of scope for this proposal but the proposed API can work with such a system (see examples).

Multiple log destinations

Finally, the API package will offer a solution to log to multiple LogHandlers at the same time through the MultiplexLogging facility. Let's assume you have two LogHandler implementations MyConsoleLogger & MyFileLogger and you wish to delegate the log messages to both of them, then the following one-time initialisation of the logging system will take care of this:

let multiLogging = MultiplexLogging([MyConsoleLogger().make, MyFileLogger().make])
Logging.bootstrap(multiLogging.make)
15 Likes

Awesome proposal. Can't wait to give this package a try. :+1:

I think this looks great! Feels like a complete logging implementation. +1

Looks great! +1

Looks amazing, ship it! :smile:

Looks great!

Just a small feedback, would it be nicer to use array instead of list in the MetadataValue enum cases?
It looks much consistent to me because Swift uses Array as the type name.

public enum MetadataValue {
    case string(String)
    indirect case dictionary(Metadata)
    indirect case array([Metadata.Value])
}
3 Likes

Thanks for working on this API! I was way too busy to provide feedback when the first draft was shared so I apologize that this is coming late in the process.

I really don't think a library should be introducing global logger or the ability to create on on the fly. If an application wants to use scoped or local loggers it is more difficult to enforce that policy when these facilities are present and trivial to use. I recommend rethinking the approach to supporting global and ad-hoc loggers such that you make it relatively easy for an application to configure that if desired while not requiring every application using this API to make these available to all application code.

The logLevel property appears to be present to configure the level at which the logger will emit messages. Is that correct? It seems like a really bad idea to expose this as a mutable property on a logger that is passed around, and a really bad idea to expose this on a global logger. Every application I have worked on wants to configure log levels centrally (per logging context) and only allow other code to produce messages (without being able to modify the level at which the logger emits those messages).

It isn't clear to me why this dictionary is optional. I think there should be pretty strong motivation for making it optional.

It isn't clear to me why we need both the subscript and a mutable property. Given the metadataKey label on the logger it is actually shorter to just use the dictionary directly:

logger[metadataKey: "foo"] = "something"
logger.metadata["foo"] = "something"

Why are these enum cases indirect? The don't need to be since the recursive storage is already heap allocated (by the collections).

I think this design is missing support for a crucial kind of metadata, at least at the individual entry level. The logging system in the library I work on right now supports including dumps of contextually relevant values along with the log entry using a CustomLogStringConvertible protocol. (I think something like this was discussed in the previous thread)

protocol CustomLogStringConvertible {
    var logDescription: String { get }
}

Individual types are expected to ensure that the logDescription does not contain any sensitive data. If you include this protocol in the library itself then the MetadataValue enum can include a case custom(CustomLogStringConvertible). The call site syntax we use is as follows:

logger.debug("preparing to start live network request...", request, otherRelevantValue)
logger.debug("something went wrong with my request...", request, error)

We use variadics here which do not support @autoclosure. This isn't a problem because the intent is to capture contextually relevant values that already exist. We don't access the logDescription unless the log message is actually emitted. A more conservative design could use N overloads to get the same syntax or could use @autoclosure and require an array at the call site:

logger.debug("preparing to start live network request...", request, otherRelevantValue)
logger.debug("something went wrong with my request...", request, error)

Your Logging.Metadata design currently requires all metadata to have keys. Our system initially required keys for the contextual values but that turned out to be redundant as the logDescription output is self-describing.

One way to combine these approaches would be to define a Logging.messageContextMetadataKey: String constant and provide overloads which package up the "keyless" entry metadata into a MetadataValue.list that is stored in that constant key. The other approach would be to just allow individual log entries to receive a MetadataValue instead of requiring a keyed Logging.Metadata (which is already supported by the MetadataValue enum and would still be available when desired).

3 Likes

The links to the examples hit a 404 since the examples have moved a little bit.
The examples are all in https://github.com/weissi/swift-server-logging-api-proposal/tree/master/Sources/ExampleUsage

I'll reply with a review / answering the requested questions shortly :slight_smile:

+1
Nice, simple API.

Let’s make it complicated :blush: :

Unfiltered Log Function

I’d like to see a log function that logs at a given log level, ignoring the log level configured for the Logger instance.

func unfiltered(level: LogLevel, message: String, metadata: Logging:Metadata? = nil, error: Error? = nil)

(possible names: log, forced, unfiltered, ...)

Rational

Experience tought me that version information is the most important information in the log (but there are environments where they run a zero error policy, so one cannot use that log level). It is hard to ensure the version logging takes place before the log level is set, e.g. if versioned modules get dynamically loaded.

Hierarchical Loggers

Logger should include func make(_ label: String) -> Logger.

Rational

Basic logger handlers could just forward that call to Logging.make but advanced loggers might store and show the chain of loggers in the log (and inherit/limit log levels).

You still can create root loggers (the "thin air case") but if you know your parent, you should have a way to make use of it.

1 Like

I forgot to mention this in my post yesterday. I use derived loggers to create a new logging context when initializing a subsystem and strongly recommend including this capability. You should also be able to pass additional Logging.Metadata that the derived logger will use. Ideally the derived logger would not expose the initial metadata to users of the derived logger and would not allow them to override the initial metadata, therefore always including the initial metadata in log messages emitted by the derived logger.

This brings to mind a couple questions about the behavior of the Logger struct more generally. Specifically, does the metadata get passed by value? If I pass a logger to a function and that function writes to metadata will that modified metadata be visible to me when the function returns (and used in my subsequent log statements)? I certainly hope not, but it isn't clear.

Usually metadata is relevant only in a specific context. Mutable metadata will encourage people to set metadata and then remove it (possibly using defer) but there is no guarantee they do. An alternative design would be to only expose the ability to derive new loggers and not expose metadata directly at all. Instead, when a new logging context (with new metadata) is required you just derive a new logger and the original logger (and all of its metadata) remains unmodified. Unless there is really strong motivation behind the need for mutable metadata I think this design should be seriously considered.

3 Likes

Along the same line, it should be possible to provide metadata at bootstrap time which is then included with every Logger dispensed.

4 Likes

Would it make sense to include an init/make that accepts parameters for things like level and metadata?

Yes it is easy to create your logger and then modify level/metadata yourself, but why not just do it at initialization?

ie:

let logger = Logging.make("com.example.app", level: .warn, metadata: requestID)

instead of:

let logger = Logging.make("com.example.app")
logger.level = .warn
logger.metadata = requestID

Also, did you consider making Logging.bootstrap accept a variadic? So rather than needing to explicitly create a MultiplexLogging object, you can just Logging.bootstrap(Logger1.make, Logger2.make)

3 Likes

Many other languages provide the ability to support custom logging levels. I see that the Logging.Level enum uses an Int raw type, would anyone else think it worthwhile to support custom logging levels?

ie:

public enum Level {
    public var rawValue: Int {
        switch self {
            case .custom(let level): return level
            case .trace: return 0
            case .debug: return 10
            case .info: return 20
            case .warning: return 30
            case .error: return 40
        }
    }
    case trace, debug, info, warning, error
    case custom(Int)
}

Or:

public struct Level: RawRepresentable, ExpressibleByIntegerLiteral {
    public let rawValue: Int

    public static let trace: Level = 0
    public static let debug: Level = 10
    public static let info: Level = 20
    public static let warning: Level = 30
    public static let error: Level = 40
    public static func custom(_ level: Int) -> Level {
        return Level(rawValue: level)
    }

    public init(integerLiteral value: Int) {
        self.init(rawValue: value)
    }

    public init(rawValue: Int) {
        self.rawValue = rawValue
    }
}

Pros:

  • Developers can make their own logging levels for increase control over their logs
  • struct version can easily conform to Comparable also

Cons:

  • Added complexity the Level code
  • The struct version is not quite as easy to switch over as the enum
  • The enum version now has to compute the rawValue every time for its Comparable conformance

Another option is to make Logger generic over a custom Comparable log level type that has factories for the standard levels (i.e. debug, error, etc)

1 Like

Thank you! Yes, that was an oversight. I'll make a small PR later fixing small things like this.

Thank you!

Thanks for still sending feedback. We had two discussion threads before (here and there). Tomer and I compiled an API out of that which should allow all existing solutions to migrate to this. This is not the final version of the implementation but the rough shape of this proposal is more or less final and the outcome of previous discussion threads. There are a things I'm personally not very happy with - some of which you mention in your post - but I believe logging is so fundamental that we need to come up with an API that everybody can use and adopt without too much friction. So asking everybody to re-architect would not work I believe. Therefore, we propose the current version to the community and the SSWG will decide whether to move along with this proposal or not.

But: If accepted into the SSWG's process, the resulting package will be a regular open-source SwiftPM package and the code will evolve over time. So implementation issues and details can be addressed whilst the proposed package is going through the Sandbox/Incubation stages of the SSWG process.

I agree with your sentiment and share your opinion. However, after talking to many people and going through two discussion threads it doesn't appear like we can force the ecosystem into some logging model without risking the API package not being widely adopted which is the main important point of an API package. One of the popular packages for example only offers a global logging facility. Other people want one logger per class and configure those with different logging levels so the ability of being able to create loggers is something we need at this stage I believe.

Well, for a value-typed LogHandlers that are explicitly passed around I think this is all good. For reference-typed LogHandlers, especially global ones I do agree with your sentiment. I know we're squaring the circle here and trying to get many different ways of logging into one API but we can't ignore what's there right now and the requirements people have. We need to rely on libraries not messing with the log level etc. My hope is that over time best practices will be established and the ecosystem itself evolves and we can evolve the APIs to capture the best practices more precisely.

It's not, that's a typo in the markdown only. I'll fix later today along with other typos. In the first proposal it was indeed optional because we were toying with the idea of allowing LogHandlers to not support metadata at all. But I think that was a bad idea and therefore we removed it.

The dictionary is meant as the primary way of storing metadata in your program, ie. the LogHandler may store it however it wants (maybe a dictionary, maybe a tree, whatever). The property that exposes the whole metadata as a dictionary and it's there to support MDC (mapped metadata context) where the context can be saved/restored to/from a thread-local variable. Again, personally not a fan of that but it's a common request that you can use a thread-local to store the metadata and when crossing threads you'd take the metadata out, ship it over to another thread and install it there...

Thanks, just by accident, will fix in code.

I think that might turn out to be a good idea. If this proposal is accepted, I'd be more than interested to work with you on getting that into the codebase before we leave the Sanbox stage. At some point obviously we'll have a public API so breaking changes will be more problematic. But in the early Sandbox stage we won't immediately declare version 1.0.0, so I'd say this is a more than acceptable change. This feels inline with the spirit of the proposal, so we don't need to propose a whole new version for that.

That is also something I'd like to talk about should this proposal be accepted.

It depends on the LogHandler, squaring the circle...

I consider this a small enough change that I would encourage a PR if this proposal gets accepted.

This is borderline but I think we could make this a PR or if it's too much of a change propose this as a follow-up change. What do you think?

I think this is a worthwhile small addition that can be proposed as a PR should this proposal get accepted as it's within the spirit of this proposal.

I think this can make sense but also has serious downsides, the ones you mentioned and more opportunity for abuse. We have deliberately decided not to provide this functionality.

3 Likes