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

Expose an API for configuring basic stdio log handling #61

Merged
merged 12 commits into from
May 1, 2019

Conversation

allenhumphreys
Copy link
Contributor

@allenhumphreys allenhumphreys commented Apr 17, 2019

Expose StreamLogHandler to the public which replaces the previously internal StdoutLogHandlers. Users can choose now the output stream, while stdout is the default.

Motivation:

As discussed on #51 and #60

Modifications:

Rename StdoutLogHandler to StreamLogHandler and provide 2 factory methods standardOutput(label:) and standardError(label:) for using during bootstrapping.

Result:

Users who adopt other logging backends during LoggingSystem bootstrapping will still be able to use the default log handler to get very basic console output to their choice of stdout or stderr.

Example:

// Log to both stderr and stdout for good measure
LoggingSystem.bootstrap { 
    MultiplexLogHandler([
        FancyLoggingBackend(label: $0),
        StreamLogHandler.standardError(label: $0), 
        StreamLogHandler.standardOutput(label: $0)
    ])
}

@swift-server-bot
Copy link

Can one of the admins verify this patch?

1 similar comment
@swift-server-bot
Copy link

Can one of the admins verify this patch?

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.

LGTM, thanks a lot for spotting it and the PR! 👍

@tomerd
Copy link
Member

tomerd commented Apr 17, 2019

@swift-server-bot test this please

@weissi weissi added the 🔼 needs-minor-version-bump For PRs that when merged cause a bump of the minor version, ie. 1.x.0 -> 1.(x+1).0 label Apr 17, 2019
@tomerd
Copy link
Member

tomerd commented Apr 17, 2019

we may want to finalize #60 before making it public?

@tomerd
Copy link
Member

tomerd commented Apr 18, 2019

@allenhumphreys would you mind changing this PR following the suggestion in #60 (comment), ie create a public generic StdioLogHandler that can be configured to log to either stdout or stderr

@allenhumphreys
Copy link
Contributor Author

@tomerd I can give it a shot. I briefly looked into the correct way to do this in Swift the other day, stdout and stderr are UnsafeMutablePointer<FILE>. I think I can effectively just replace print(...) with fputs(..., stderr|stdout). That about the gist of it?

@allenhumphreys
Copy link
Contributor Author

Digging into the stdlib a little bit.. using something like:

extension UnsafeMutablePointer: TextOutputStream where Pointee == FILE {
    public mutating func write(_ string: String) {
        fputs(string, self)
    }
}

with

print(..., to: &stderr)

might be better but I don't know why you can't print to stdout as part of the stdlib in the first place.

@tomerd
Copy link
Member

tomerd commented Apr 18, 2019

@allenhumphreys
Copy link
Contributor Author

Oh good point, I hadn't seen that portion of it! Thank you!

Copy link
Member

@tomerd tomerd left a comment

Choose a reason for hiding this comment

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

this is shaping up nicely. couple of comments. add some tests too?

@tomerd tomerd requested a review from ktoso April 24, 2019 02:06
@allenhumphreys
Copy link
Contributor Author

@tomerd Tests will have to come on a different day!

Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

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

Thank you! That looks great.

@weissi weissi added this to the 1.1.0 milestone Apr 24, 2019
@tomerd
Copy link
Member

tomerd commented Apr 25, 2019

@swift-server-bot test this please

@tomerd
Copy link
Member

tomerd commented Apr 25, 2019

@allenhumphreys looks like tests are failing on linux. you can run them locally with docker-compose -f docker/docker-compose.yaml -f docker/docker-compose.1804.50.yaml run test

internal struct StdoutLogHandler: LogHandler {
public struct StdioLogHandler: LogHandler {

public enum Stream {
Copy link
Member

Choose a reason for hiding this comment

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

@allenhumphreys just as a question if you think this is useful. We could also do the following here: Instead of an enum Stream we could do

// this is just to prevent name clashes further down
#if os(macOS) || os(tvOS) || os(iOS) || os(watchOS)
let sysStderr = Darwin.stderr
let sysStdout = Darwin.stdout
#else
let sysStderr = Glibc.stderr
let sysStdout = Glibc.stdout
#endif

public struct Stream {
    private let underlying: UnsafePointer<FILE>

    internal init(underlying: UnsafePointer<FILE>) {
        self.underlying = underlying
    }

    public static let stderr: Stream = .init(underlying: sysStderr)
    public static let stdout: Stream = .init(underlying: sysStdout)
}

that way, the API is the same for the user: StdioLogHandler(label: "foo", stream: .stdout) will still work but we have two extra benefits:

  1. in tests, we can use StdioLogHandler(label: "foo", stream: .init(underlying: fopen(".....", "w+")))
  2. if we chose to allow other streams in the future, then we can add them without breaking public API. The problem with enums is: Adding a case breaks the API because someone might have done an exhaustive switch over it. The struct + static let solution from above alleviates that issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @weissi , this is good, but I'd like to propose going a little further.

  1. Make Stream wrap TextOutputStream instead
  2. Rename StdioLogHandler to StreamLogHandler
  3. Go ahead and expose the initializer for Stream

It'd look something like this:

public struct StreamLogHandler: LogHandler {

    public struct Stream: TextOutputStream {

        private let underlying: TextOutputStream

        public init(underlying: TextOutputStream) {
            self.underlying = underlying
        }

        public mutating func write(_ string: String) {
            var underlying = self.underlying
            underlying.write(string)
        }

        public static let stderr: Stream = .init(underlying: FileOutputStream(file: sysStderr))
        public static let stdout: Stream = .init(underlying: FileOutputStream(file: sysStdout))
    }

    private let lock = Lock()
    public let stream: Stream

    public init(label: String, stream: Stream = .stdout) {
        self.stream = stream
    }

...
...
}

FileOutputStream would remain internal, but clearly available as a reference for consumers. This would allow people to immediately swap in their own streams. It eliminates the need to work with Pointers, or even the c standard library at all. Stream ends up's up just being a box and a convenient way to get access to stderr and stdout in a way that hides the UnsafeMutablePointer<FILE> stuff.

Thoughts? Too far?

Copy link
Member

Choose a reason for hiding this comment

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

@allenhumphreys Thanks, that sounds like a great idea to me. I think you can remove the Lock because if the user wants locking, they could implement that themselves, no?

Copy link
Member

Choose a reason for hiding this comment

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

Seems like a good idea 👍

Copy link
Member

Choose a reason for hiding this comment

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

@allenhumphreys Thanks, that sounds like a great idea to me. I think you can remove the Lock because if the user wants locking, they could implement that themselves, no?

Is that a safe default? in case a concurrent program would use the default loggers shipped?

Copy link
Member

Choose a reason for hiding this comment

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

@allenhumphreys ah, you can remove that lock. LogHandlers are supposed to have value semantics for the configuration (metadata & logLevel) and indeed your handler has, so there's no need to lock.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@weissi Thanks for pointing that out, it looks like the lock was a holdover from when StdoutLogHandler was a class, changed at 1d98315

Copy link
Member

Choose a reason for hiding this comment

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

@allenhumphreys thank you! Apologies, for some reason I thought you added the lock. I'd propose to just remove it because it's baggage from the past :).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In light of discovering how Swift.print functions, I tweaked the API again. I removed the wrapper type StreamLogHandler.Stream and exposed StdioOutputStream to allow easily switching between stderr and stdio. StdioOutputStream is not publicly instantiable, though, so the usage is still restricted. I believe this API is now much more straightforward as it just uses TextOutputStream!

Copy link
Member

Choose a reason for hiding this comment

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

@allenhumphreys perfect. I think it you make the output streams non-public, then we can merge this.

@allenhumphreys
Copy link
Contributor Author

@tomerd @ktoso @weissi I have updated the implementation, the documentation I've proposed isn't complete yet, but I've laid out the general direction. I've left the locking behavior the same per my last comment on the thread.

Thanks everyone for continuing to help evolve the direction of this change.

@tomerd
Copy link
Member

tomerd commented Apr 30, 2019

@swift-server-bot test this please

/// A wrapper to facilitate `print`-ing to stderr and stdio that
/// ensures access to the underlying `FILE` is locked to prevent
/// cross-thread interleaving of output.
public struct StdioOutputStream: TextOutputStream {
Copy link
Member

Choose a reason for hiding this comment

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

The StdioOutputStream we should leave internal or private as that's a thing the stdlib should really provide.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I have a similar feeling but keeping it private requires some kind of additional wrapper type or switch to power StreamLogHandler's public initializer (fundamentally providing the consumer the ability to switch between stderr and stdout).

As I had it before StreamLogHandler.Stream also conformed to TextOutputStream.

Previously it was

public static let stderr: Stream = .init(underlying: FileOutputStream(file: systemStderr))

now this has the same level of access:

public static let stdout = StdioOutputStream(file: systemStdout)

but with less indirection.

Ultimately something has to be public in that initializer because one of the goals of this change is to allow easily redirected to stderr. Alternatively, to keep the type private, I could have multiple specialized initializers or possibly static factory methods to provide this functionality.

Copy link
Member

Choose a reason for hiding this comment

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

@allenhumphreys if you overload the init of StreamLogHandler instead of using default arguments, it'll work. All the streams stuff can stay internal/private. So

public struct StreamLogHandler { 
    ...
    public init(label: String) {
        self.init(stream: StdioOutputStream(stream: sysStdout))
    }

    public init(label: String, stream: TextOutputStream) {
        ...
    }
    ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That doesn't leave a good name for an init that goes to stderr, any suggestion good or not as good?
Just thinking out loud:

init(toStdoutWithLabel label: String)
init(toStderrWithLabel label: String)

Copy link
Member

Choose a reason for hiding this comment

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

@allenhumphreys Ahh, I see! I'd then go with

public struct StreamLogHandler {
    public static func makeStdoutLogHandler(label: String) -> StreamLogHandler { ... }
    public static func makeStderrLogHandler(label: String) -> StreamLogHandler { ... }

   ...
}

and then we can even leave the init private

@allenhumphreys allenhumphreys changed the title Expose StdoutLogHandler to the public Expose an API for configuring basic stdio log handling Apr 30, 2019
Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

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

Awesome, thanks! I'm happy with this.

@weissi
Copy link
Member

weissi commented Apr 30, 2019

@swift-server-bot test this please

@weissi weissi requested a review from tomerd April 30, 2019 17:01
@weissi
Copy link
Member

weissi commented Apr 30, 2019

@ktoso / @tomerd / @ianpartridge I'm happy exactly as is, would you mind having a quick look? Just because there were a lot of changes.


public var logLevel: Logger.Level {
/// Factory that makes a `StreamLogHandler` to directs its output to `stdout`
public static func makeStdoutLogHandler(label: String) -> StreamLogHandler {
Copy link
Member

Choose a reason for hiding this comment

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

naming nit: i like the new factories but dont love the names. maybe makeStdout and makeStderr is enough. in any case not a big deal, so can go with this too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, it is a bit repetitive in the full context StreamLogHandler.makeStdoutLogHandler, happy to change. While you're considering names, I made up the StreamLogHandler name to convey it's new expanded capabilities, but it could just as well be TextOutputStreamLogHandler :-)

Copy link
Member

Choose a reason for hiding this comment

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

naming is hard. @weissi any preferences?

Copy link
Member

Choose a reason for hiding this comment

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

@tomerd not really but I agree with both of you that makeStdoutLogHandler is a bit long and redundant. We could even do something like standardOutput only. Then it would read

Logging.bootstrap(StreamLogHandler.standardOutput)

makeStdout sounds like it's making a 'stdout' to me...

Copy link
Member

@tomerd tomerd left a comment

Choose a reason for hiding this comment

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

lgtm and thank you @allenhumphreys

Copy link
Member

@weissi weissi left a comment

Choose a reason for hiding this comment

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

AWESOME, thanks, this looks great!

@weissi
Copy link
Member

weissi commented May 1, 2019

@swift-server-bot test this please

@weissi weissi merged commit ea3eea9 into apple:master May 1, 2019
@tomerd
Copy link
Member

tomerd commented May 1, 2019

<3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔼 needs-minor-version-bump For PRs that when merged cause a bump of the minor version, ie. 1.x.0 -> 1.(x+1).0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants