Skip to content
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

add source for log messages #135

Merged
merged 2 commits into from
Jul 6, 2020
Merged

add source for log messages #135

merged 2 commits into from
Jul 6, 2020

Conversation

weissi
Copy link
Member

@weissi weissi commented Jun 5, 2020

Motivation:

We noticed that we need log source information, which is a stable and greppable identifier for where a log message originated from. But less precise than file/function but about as or slightly more precise than the current module.
Some folks used label for this purpose but instantiating new loggers everywhere just to get a good label means the following sub-optimal things:

  • metadata propagation more complicated than necessary
  • setting logLevel for a called library not possible (unless extra parameter passed)
  • controlling the logging backend for a called library not possible (unless yet another parameter passed)
  • the loss of label as an identifier for a part of an application (eg. if your binary contains two independent services ServiceA and ServiceB but both use a library such as AsyncHTTPClient. Then ideally you'd have label: "MyApp.ServiceA" and label: "MyApp.ServiceB" to separate messages originating from AHC.

All of these things are solved and behave very nicely if you pass a "root logger" around to all sub-systems. The problem however is if you cannot use label as source information, where do you get source information from?

Modification:

  • introduce a new source field for every log message that defaults to the calling module (currently parsing #file until SR-12920 is resolved)
  • introduce LoggerWithSource which wraps a Logger always attaching a non-standard source. For example in AHC you may want to make a LoggerWithSource(obtainedLogger, source: "AHC.ConnectionPool") for logs in the connection pool.

Result:

Proper source identifier.

@weissi weissi added the semver/minor Adds new public API. label Jun 5, 2020
@avolokhov
Copy link

Do we want to provide a default factory? E.g. var logger = logger.withSource(source: source).

@ktoso ktoso self-requested a review June 8, 2020 01:15
@ktoso
Copy link
Member

ktoso commented Jun 8, 2020

Checking this out today in a project; I have some concerns but let's see how it goes.

@inlinable
public func debug(_ message: @autoclosure () -> Logger.Message,
metadata: @autoclosure () -> Logger.Metadata? = nil,
source: @autoclosure () -> String? = nil,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's some copy/paste leftover: we use self.source, and not the one provided here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for all other methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These signatures are wrong in one or another way I guess indeed:

a) We want to keep exact same method signatures as Logger:

  • so we keep source: @autoclosure () -> String? = nil,
  • impl has to source: source() ?? self.source
  • it must allow `sourcelogger.info("", source: "actually-more-specific-ad-hoc")
    • if we want to keep this parameter here indeed

or

b) Like Anton mentions, this parameter should go away on all functions on LoggerWithSource

b) seems to make more sense, as Anton says. Either's fine tbh, but the current impl is not consistent with what the signature advertises we're able to do with these functions (override the source anyway)

public func trace(_ message: @autoclosure () -> Logger.Message,
metadata: @autoclosure () -> Logger.Metadata? = nil,
file: String = #file, function: String = #function, line: UInt = #line) {
self.logger.trace(message(),
Copy link

@avolokhov avolokhov Jun 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this, but I think LoggerWithSource.trace should delegate to LoggerWithSource.log, and not to Logger.trace. Otherwise LoggerWithSource seems inconsistent with Logger, and the reason for LoggerWithSource.log is unclear. WDYT? Same for other methods.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@avolokhov From the generated code's perspective this is indistinguishable so we can change at any point in time.

The reason I did add log with LoggerWithSource is that some people may be using log(level: .trace, ...) instead of .trace and I want it to be compatible.

@ktoso
Copy link
Member

ktoso commented Jun 9, 2020

Quoting @weissi from #133 (comment)
(my bad, replied in wrong PR there)

Status update — I think this likely will be fine but I’ve ran out of time in the day to get it to completion in my project to really see it work out end to end.
It makes the label pretty useless but I guess we’ll agree that’s what it is. I’ll post a review tomorrow

I thought about label more and I actually now think it's useful. It's basically orthogonal to source.

Let's consider a whole system and let's assume the system offers 2 services, let's call them service A and service B. Services A & B both use a multitude of sub-subsystems. Then everything that belongs to Service A gets label=MyApp.ServiceA and B gets label=MyApp.ServiceB. If now both A & B use say AsyncHTTPClient, then the label suddenly becomes useful.

  • source=AHC gives us AHC calls from both A & B which might be annoying if I'm a developer working only on A
  • source=AHC & label=MyApp.ServiceA however gives me exactly what I want.

That's a good point, it actually works out well there 👍

Copy link
Member

@ktoso ktoso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried it out "in action" and do think it'll work well enough 👍
Glad we talked through the use-cases and I think this now can serve all patterns.

I'm a bit worried about the string parsing module workaround... but otherwise looks good; just minor comments on API consistency and some docs 👍

Sources/Logging/LoggerWithSource.swift Outdated Show resolved Hide resolved
logger.critical("yes: critical")

testLogging.history.assertExist(level: .critical, message: "yes: critical", source: "source")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@inlinable
public func debug(_ message: @autoclosure () -> Logger.Message,
metadata: @autoclosure () -> Logger.Metadata? = nil,
source: @autoclosure () -> String? = nil,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These signatures are wrong in one or another way I guess indeed:

a) We want to keep exact same method signatures as Logger:

  • so we keep source: @autoclosure () -> String? = nil,
  • impl has to source: source() ?? self.source
  • it must allow `sourcelogger.info("", source: "actually-more-specific-ad-hoc")
    • if we want to keep this parameter here indeed

or

b) Like Anton mentions, this parameter should go away on all functions on LoggerWithSource

b) seems to make more sense, as Anton says. Either's fine tbh, but the current impl is not consistent with what the signature advertises we're able to do with these functions (override the source anyway)

Sources/Logging/Logging.swift Show resolved Hide resolved
Sources/Logging/Logging.swift Show resolved Hide resolved
Tests/LoggingTests/LoggingTest.swift Outdated Show resolved Hide resolved
Sources/Logging/Logging.swift Show resolved Hide resolved
Sources/Logging/LogHandler.swift Show resolved Hide resolved
Sources/Logging/LogHandler.swift Show resolved Hide resolved
@avolokhov
Copy link

avolokhov commented Jun 9, 2020

Let's consider a whole system and let's assume the system offers 2 services, let's call them service A and service B. Services A & B both use a multitude of sub-subsystems. Then everything that belongs to Service A gets label=MyApp.ServiceA and B gets label=MyApp.ServiceB. If now both A & B use say AsyncHTTPClient, then the label suddenly becomes useful.

source=AHC gives us AHC calls from both A & B which might be annoying if I'm a developer working only on A

@ktoso To this point, wouldn't it make label just a required metadata field? We can achieve this with metadata only, why give multiple ways to do the same?

@weissi
Copy link
Member Author

weissi commented Jun 9, 2020

@ktoso To this point, wouldn't it make label just a required metadata field? We can achieve this with metadata only, why give multiple ways to do the same?

Well, it's unchangable. So if you implemented metadata swapping, the label would not be swapped. Also we don't have required metadata fields right now. But I agree it's kinda similar.

@avolokhov
Copy link

I'm a bit worried that source fully relies on a library developer being a good citizen, we don't enforce it anywhere. In a perfect world of ultimate tyranny, I'd just remove all logging methods from logger and keep them in only in loggerWithSource so that a developer is forced to add source to be able to log something. Sad we can't do it.

@weissi
Copy link
Member Author

weissi commented Jun 9, 2020

I'm a bit worried that source fully relies on a library developer being a good citizen, we don't enforce it anywhere. In a perfect world of ultimate tyranny, I'd just remove all logging methods from logger and keep them in only in loggerWithSource so that a developer is forced to add source to be able to log something. Sad we can't do it.

Yes, we can't enforce this at this point in time anymore.

@ktoso
Copy link
Member

ktoso commented Jun 9, 2020

[...] remove all logging methods from logger and keep them in only in loggerWithSource so that a developer is forced to add source to be able to log something. Sad we can't do it.

I'm not sure that's the answer either, there's much to say for "I really don't care, I'm just a simple app" use cases where the default = #module really is kind of good enough already anyway. The source is really something usable that label today isn't for libraries which do have fine grained opinions about log sources, not sure forcing everyone into it is strictly necessary;

We discussed options at length with Johannes but let me note down some of them here and why they break down and we're left with a source thing.

Strawman 1: What if... we allowed changing label

One could argue that "use label, make it changeable" which i think betrays intentions though, since Johannes wants to "create one logger and pass it to sub-systems" which e.g. would be one such library which has strong opinions about subsytems / sources.

If we just said, "just allow to change the source" the following API is kind of lying IMHO:

library.logger = Logger(label: "AwesomeApp")
// first thing library would do is...
// let subSystemLogger = library.logger.withLabel("subsystem-x")

so... when users passed AwesomeApp, they hare the reasonable expectation this label will show up in logs and can grep or search by it. But... if label is changeable they couldn't -- since that's the "one spot" for log sources...

That's very bad / confusing. So... making label changeable is not an answer.

Strawman 2: label "IS" source

To be honest that's how I've seen it, but it requires libraries which have strong opinions about "what is logging right now" to create loggers in the library.

In order to avoid the global factory and allow swapping impls per library instance (that's a hard requirement IMO), one has to in the library then set a (String) -> Logger which defaults to Logger.init(label:)... That's what I've been doing, along with configuration what the loglevel and metadata for those "created inside the library" loggers should be.

This works if configuration suits people; and while it suits me, there are others which really like the "create a root logger and pass it into libraries" If we did this with the "label is the source of the entity that is logging" then it breaks down -- label can't be changed.

Strawman 3: Just metadata

Also an option I guess... though we can't do the "please DO set a source" maybe it's harder to get right than the type at all...

It would be better (?) in the sense that it does not cause another type to be created for the "i know the source" cases. Though on the other hand when I ported my work to use this WIP PR the "I know i have a specific source" type was useful during library development... Though I kind of expect few developers to care about this? I do imagine it matter a lot through in very concurrent settings where one wants to log "which threadpool" or whatnot is performing the actions, so the log would contain these as sources perhaps... All in all, it's helpful if source is a "real" thing we talk about, and not just a pattern.

What convienced me was Johannes's point recently

Services A & B both use a multitude of sub-subsystems. Then everything that belongs to Service A gets label=MyApp.ServiceA and B gets label=MyApp.ServiceB. If now both A & B use say AsyncHTTPClient, then the label suddenly becomes useful.

Strawman 4: "never use the create 1 root logger and pass it into libraries"

For the record that was my initial stance: all libs would have to make customization points for log level, "root" metadata, and "mocking" a logger out...

So libraries have to expose defaultLogLevel and defaultMetadata that they would use thenever they create a logger using Logger(label: ...) using the "global" convenience initializer. This does lead to global state though, so libraries also have to invent the (String) -> Logger configuration point I mentioned.

While I trust libraries we build could get this right and maybe even consistent if we try hard enough, it's a lot of specific things a lib has to expose, and it defeats a pattern some of us like (the "pass rootLogger into library". And I do agree it'd probably never happen that "random libraries" use the same specific pattern and ways to configure this...At-best they might develop the same customization points, but all in slightly different syles and names... or at worst ignore this completely ("global state? what's the problem!" :P) and we're back in global state land making logging when many instances of a lib in one process exist difficult.


So... we ended up with "what if we add source" which is the library controlled "source" meaning "the entity that is logging". It's default being the module is good enough and kind of the right thing always for small libraries, and larger libraries with subsystems and background tasks, thread pools etc can control if it they want to. It's in the control of library authors, but really that's what we're after here -- the user controlled label "MyCoolSystem" is "label", and "source" is up to library internals. I.e. no "top level logger" should ever be the LoggerWithSource.

Uff, so yeah, that's pretty much a summary of the various takes we had on this I think. I've had my reservations but after many chats and also trying it out in action now I guess it's an okey tradeoff.


It does still mean setting that then passing "between" / "through" libraries the source is not really propagated... it's always the lib's "own" sources we'll see. So for execution contexts/thread pools I guess one has to set the metadata anyway always after all 🤔 Otherwise I would not know that I'm on pool X, and executing thingy library Y, since it accepted Logger and thus lost "my" source which i used to mark the pool...

So... perhaps source is not solving the problem after all, or is it...? 🤔

@avolokhov
Copy link

Oh, I think we should emphasise that source should be stable. I already imagine some library goes mad and set uuid as a source, making it impossible to filter it out by source.

@ktoso
Copy link
Member

ktoso commented Jun 9, 2020

Yeah good point, likely we should document that hint.

@ktoso
Copy link
Member

ktoso commented Jun 10, 2020

Minor additional point I just noticed, I seem to do a lot of

log.logger.withSource("serialization")

where log is a libraries "top level logger" (the one someone could set when they start my library). Would it make sense to also offer withSource on the LoggerWithSource? @weissi

@weissi weissi force-pushed the jw-source branch 2 times, most recently from 2ba7a52 to 65d928b Compare June 22, 2020 14:25
@weissi weissi marked this pull request as ready for review June 22, 2020 14:25
@weissi weissi requested review from ktoso and tomerd June 22, 2020 14:25
@ktoso
Copy link
Member

ktoso commented Jun 22, 2020

So in general this looks good to me. I remain worried about the "faking that we have #module" esp with longer module names... Will try to sanity check this soon.

Do you want to merge this soon or would we want to know what way forward wrt #module (or similar) is?

Context: I have plenty of logs on .trace level and I'm worried that that source workaround would actually allocate/slow-down runtime a lot in my lib. What if a real thing never lands etc... So I'd like to sanity check that before LGTMing. (or some benchmark in here etc)

@ktoso
Copy link
Member

ktoso commented Jun 22, 2020

Minor, what about: #135 (comment)

Copy link
Member

@ktoso ktoso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized my main concern was not necessary in #135 (comment) thanks again @weissi

LGTM as far as I can tell 👍

@weissi
Copy link
Member Author

weissi commented Jun 22, 2020

@ktoso / @tomerd API digester is wrong: https://bugs.swift.org/browse/SR-11859

@weissi
Copy link
Member Author

weissi commented Jun 22, 2020

@avolokhov for some reason, Github's "request re-review" button doesn't seem to work for you so I'm @-ing you

Copy link

@avolokhov avolokhov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I accidentally requested changes on the very first iteration. Looks good!

@ktoso
Copy link
Member

ktoso commented Jun 22, 2020

Does the API check being wrong though mean we'll always have it fail when we merge this since it compares prev-release to current?

In Akka (the MiMa tool) we had "ignore these checks" if we indeed "broke" but knew the change is ok for some reason: https://github.com/akka/akka/blob/master/akka-actor/src/main/mima-filters/2.5.2.backwards.excludes do we need a similar thing to keep the api-digester being always wrong and highlighting such things?

@weissi
Copy link
Member Author

weissi commented Jun 22, 2020

@ktoso no, it'll only fail on this very PR. It always compares commit N vs. N+1.

@ktoso
Copy link
Member

ktoso commented Jun 23, 2020

Okey, seems we're good then

@weissi
Copy link
Member Author

weissi commented Jun 23, 2020

@tomerd mind having a look here

@tomerd
Copy link
Member

tomerd commented Jun 29, 2020

thanks for putting this together @weissi

As we discussed in the original pitch and proposal, there are many usage patterns for SwiftLog (propagation, local or global to name the main ones), and a key requirement from it is to be able use configuration [at the logger-backend level] to control logging verbosity of specific subsystems and libraries while troubleshooting.

I think the combination of label + source is a good way to drive such configuration, even better than just label + metadata that we have now, so +1 on adding this. I do think it would be beneficial to better document this concept to help guide logging-backend authors to offer such configuration options. As t stands this concept is buried in a test utility which is not used in practice, so hard to find.

With that said, I do not think we should expand the API surface area with LoggerWithSource, I feel most libraries won't need it and its trivial enough to add locally in those that do. If we see this is not the case, and many library authors request this feature we can add it at a later stage.

cc @ktoso

@ktoso
Copy link
Member

ktoso commented Jun 30, 2020

I agree with the sentiment, let's take it slow with adding the new type.

I initially thought i'd need it but in reality only used it in 2 source files so it's not been as crucial as I thought initially, happy to make the wrapper myself.

We can make a ticket explaining what it is in case people will bump into it and +1 that ticket.

@ktoso
Copy link
Member

ktoso commented Jul 6, 2020

Will rebase and merge this one. The LoggerWithSource I'll pull out as proposal / ticket for now.


The ticket with the LoggerWithSource for future reference: #143 it may die out if no interest, or it may get some interest -- we'll see. Currently I don't think we'll need it but let's see what other projects will feel after a while of real world usage.

@ktoso
Copy link
Member

ktoso commented Jul 6, 2020

Removed the new LoggerWithSource type, see 8058ed5 and ticketified as #143
API validation fails but known issue and we took care about compatibility; see also #135 (comment)

Once we release we'll need to write up guidance about implementing 1.3.0 "new handler function", backends can do this "whenever they're ready to"

@ktoso ktoso merged commit 024f44d into apple:master Jul 6, 2020
@ktoso ktoso added this to the 1.3.0 milestone Jul 6, 2020
glbrntt added a commit to glbrntt/swift-log that referenced this pull request Aug 13, 2020
Motivation:

- When `source:` was added in apple#135, the API was accidentally broken
  despite source compatability.

Modifications:

- Add back source-less API calls which defer to the decls with source.
- Note we can't deprecate the calls we're adding back:
  `log.info("some message")` refers to the implementation we're adding
  back rather and would therefore require users to explicitly specify a source
  to suppress the deprecation warning message.

Result:

- API is no longer broken.
glbrntt added a commit to glbrntt/swift-log that referenced this pull request Aug 13, 2020
Motivation:

- When `source:` was added in apple#135, the API was accidentally broken
  despite source compatability.

Modifications:

- Add back source-less API calls which defer to the decls with source.
- Note we can't deprecate the calls we're adding back:
  `log.info("some message")` refers to the implementation we're adding
  back rather and would therefore require users to explicitly specify a source
  to suppress the deprecation warning message.

Result:

- API is no longer broken.
ktoso added a commit that referenced this pull request Feb 22, 2021
Motivation:

- When `source:` was added in #135, the API was accidentally broken
  despite source compatability.

Modifications:

- Add back source-less API calls which defer to the decls with source.
- Note we can't deprecate the calls we're adding back:
  `log.info("some message")` refers to the implementation we're adding
  back rather and would therefore require users to explicitly specify a source
  to suppress the deprecation warning message.

Result:

- API is no longer broken.

Co-authored-by: Konrad `ktoso` Malawski <ktoso@apple.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants