Repository: apple/swift-nio-extras
Pull request #22 (2019-01-17 23:18:36)

Add channel handlers to debug inbound and outbound events
Motivation:

Users may want to log all of inbound and/or outbound events. This addresses https://github.com/apple/swift-nio/issues/475

Modifications:

Add DebugInboundEventsHandler and DebugOutboundEventsHandler

Result:

Users can plug additional handlers into their pipeline for default printing of events or getting a hook with relevant information for their own logging mechanism.

swift-nio-bot: Can one of the admins verify this patch?
JovanMilenkovic: @Lukasa tnx for the review. Regarding "sut", it's supposed to be SYSTEM UNDER TEST and provide a clear distinction between the main variable and helper variables in the test suite. It's fairly common (https://github.com/search?q=sut&type=Code) and probably originates from https://www.istqb.org/downloads/send/20-istqb-glossary/210-istqb-glossary-3-2-ga-release-notes-final.html. I see now that it currently doesn't belong in NIO test suites (it was my force of habit) but perhaps it should be further explored, I think it pulls it's weight.
Lukasa: @swift-nio-bot test this please
weissi: @swift-nio-bot test this please

Patch:
diff --git a/Sources/NIOExtras/DebugInboundEventsHandler.swift b/Sources/NIOExtras/DebugInboundEventsHandler.swift
new file mode 100644
index 0000000..9caf82e
--- /dev/null
+++ b/Sources/NIOExtras/DebugInboundEventsHandler.swift
@@ -0,0 +1,112 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import NIO
+
+/// ChannelInboundHandler that prints all inbound events that pass through the pipeline by default,
+/// overridable by providing your own closure for custom logging. See DebugOutboundEventsHandler for outbound events.
+public class DebugInboundEventsHandler: ChannelInboundHandler {
+    
+    public typealias InboundIn = Any
+    public typealias InboudOut = Any
+    
+    public enum Event {
+        case registered
+        case unregistered
+        case active
+        case inactive
+        case read(data: NIOAny)
+        case readComplete
+        case writabilityChanged(isWritable: Bool)
+        case userInboundEventTriggered(event: Any)
+        case errorCaught(Error)
+    }
+    
+    var logger: (Event, ChannelHandlerContext) -> ()
+    
+    public init(logger: @escaping (Event, ChannelHandlerContext) -> () = DebugInboundEventsHandler.defaultPrint) {
+        self.logger = logger
+    }
+    
+    public func channelRegistered(ctx: ChannelHandlerContext) {
+        logger(.registered, ctx)
+        ctx.fireChannelRegistered()
+    }
+    
+    public func channelUnregistered(ctx: ChannelHandlerContext) {
+        logger(.unregistered, ctx)
+        ctx.fireChannelUnregistered()
+    }
+    
+    public func channelActive(ctx: ChannelHandlerContext) {
+        logger(.active, ctx)
+        ctx.fireChannelActive()
+    }
+    
+    public func channelInactive(ctx: ChannelHandlerContext) {
+        logger(.inactive, ctx)
+        ctx.fireChannelInactive()
+    }
+    
+    public func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
+        logger(.read(data: data), ctx)
+        ctx.fireChannelRead(data)
+    }
+    
+    public func channelReadComplete(ctx: ChannelHandlerContext) {
+        logger(.readComplete, ctx)
+        ctx.fireChannelReadComplete()
+    }
+    
+    public func channelWritabilityChanged(ctx: ChannelHandlerContext) {
+        logger(.writabilityChanged(isWritable: ctx.channel.isWritable), ctx)
+        ctx.fireChannelWritabilityChanged()
+    }
+    
+    public func userInboundEventTriggered(ctx: ChannelHandlerContext, event: Any) {
+        logger(.userInboundEventTriggered(event: event), ctx)
+        ctx.fireUserInboundEventTriggered(event)
+    }
+    
+    public func errorCaught(ctx: ChannelHandlerContext, error: Error) {
+        logger(.errorCaught(error), ctx)
+        ctx.fireErrorCaught(error)
+    }
+    
+    public static func defaultPrint(event: Event, in ctx: ChannelHandlerContext) {
+        let message: String
+        switch event {
+        case .registered:
+            message = "Channel registered"
+        case .unregistered:
+            message = "Channel unregistered"
+        case .active:
+            message = "Channel became active"
+        case .inactive:
+            message = "Channel became inactive"
+        case .read(let data):
+            message = "Channel read \(data)"
+        case .readComplete:
+            message = "Channel completed reading"
+        case .writabilityChanged(let isWritable):
+            message = "Channel writability changed to \(isWritable)"
+        case .userInboundEventTriggered(let event):
+            message = "Channel user inbound event \(event) triggered"
+        case .errorCaught(let error):
+            message = "Channel caught error: \(error)"
+        }
+        print(message + " in \(ctx.name)")
+    }
+    
+}
diff --git a/Sources/NIOExtras/DebugOutboundEventsHandler.swift b/Sources/NIOExtras/DebugOutboundEventsHandler.swift
new file mode 100644
index 0000000..195db63
--- /dev/null
+++ b/Sources/NIOExtras/DebugOutboundEventsHandler.swift
@@ -0,0 +1,105 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import NIO
+
+/// ChannelOutboundHandler that prints all outbound events that pass through the pipeline by default,
+/// overridable by providing your own closure for custom logging. See DebugInboundEventsHandler for inbound events.
+public class DebugOutboundEventsHandler: ChannelOutboundHandler {
+    
+    public typealias OutboundIn = Any
+    public typealias OutboundOut = Any
+    
+    public enum Event {
+        case register
+        case bind(address: SocketAddress)
+        case connect(address: SocketAddress)
+        case write(data: NIOAny)
+        case flush
+        case read
+        case close(mode: CloseMode)
+        case triggerUserOutboundEvent(event: Any)
+    }
+
+    var logger: (Event, ChannelHandlerContext) -> ()
+    
+    public init(logger: @escaping (Event, ChannelHandlerContext) -> () = DebugOutboundEventsHandler.defaultPrint) {
+        self.logger = logger
+    }
+    
+    public func register(ctx: ChannelHandlerContext, promise: EventLoopPromise<Void>?) {
+        logger(.register, ctx)
+        ctx.register(promise: promise)
+    }
+    
+    public func bind(ctx: ChannelHandlerContext, to address: SocketAddress, promise: EventLoopPromise<Void>?) {
+        logger(.bind(address: address), ctx)
+        ctx.bind(to: address, promise: promise)
+    }
+    
+    public func connect(ctx: ChannelHandlerContext, to address: SocketAddress, promise: EventLoopPromise<Void>?) {
+        logger(.connect(address: address), ctx)
+        ctx.connect(to: address, promise: promise)
+    }
+    
+    public func write(ctx: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
+        logger(.write(data: data), ctx)
+        ctx.write(data, promise: promise)
+    }
+    
+    public func flush(ctx: ChannelHandlerContext) {
+        logger(.flush, ctx)
+        ctx.flush()
+    }
+    
+    public func read(ctx: ChannelHandlerContext) {
+        logger(.read, ctx)
+        ctx.read()
+    }
+    
+    public func close(ctx: ChannelHandlerContext, mode: CloseMode, promise: EventLoopPromise<Void>?) {
+        logger(.close(mode: mode), ctx)
+        ctx.close(mode: mode, promise: promise)
+    }
+    
+    public func triggerUserOutboundEvent(ctx: ChannelHandlerContext, event: Any, promise: EventLoopPromise<Void>?) {
+        logger(.triggerUserOutboundEvent(event: event), ctx)
+        ctx.triggerUserOutboundEvent(event, promise: promise)
+    }
+
+    public static func defaultPrint(event: Event, in ctx: ChannelHandlerContext) {
+        let message: String
+        switch event {
+        case .register:
+            message = "Registering channel"
+        case .bind(let address):
+            message = "Binding to \(address)"
+        case .connect(let address):
+            message = "Connecting to \(address)"
+        case .write(let data):
+            message = "Writing \(data)"
+        case .flush:
+            message = "Flushing"
+        case .read:
+            message = "Reading"
+        case .close(let mode):
+            message = "Closing with mode \(mode)"
+            print()
+        case .triggerUserOutboundEvent(let event):
+            message = "Triggering user outbound event: { \(event) }"
+        }
+        print(message + " in \(ctx.name)")
+    }
+    
+}


Test patch:
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
index a79e557..cb57ed9 100644
--- a/Tests/LinuxMain.swift
+++ b/Tests/LinuxMain.swift
@@ -26,6 +26,8 @@ import XCTest
    @testable import NIOExtrasTests
 
    XCTMain([
+         testCase(DebugInboundEventsHandlerTest.allTests),
+         testCase(DebugOutboundEventsHandlerTest.allTests),
          testCase(FixedLengthFrameDecoderTest.allTests),
          testCase(LengthFieldBasedFrameDecoderTest.allTests),
          testCase(LengthFieldPrependerTest.allTests),
diff --git a/Tests/NIOExtrasTests/DebugInboundEventsHandlerTest+XCTest.swift b/Tests/NIOExtrasTests/DebugInboundEventsHandlerTest+XCTest.swift
new file mode 100644
index 0000000..bc5127e
--- /dev/null
+++ b/Tests/NIOExtrasTests/DebugInboundEventsHandlerTest+XCTest.swift
@@ -0,0 +1,41 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+//
+// DebugInboundEventsHandlerTest+XCTest.swift
+//
+import XCTest
+
+///
+/// NOTE: This file was generated by generate_linux_tests.rb
+///
+/// Do NOT edit this file directly as it will be regenerated automatically when needed.
+///
+
+extension DebugInboundEventsHandlerTest {
+
+   static var allTests : [(String, (DebugInboundEventsHandlerTest) -> () throws -> Void)] {
+      return [
+                ("testRegistered", testRegistered),
+                ("testUnregistered", testUnregistered),
+                ("testActive", testActive),
+                ("testInactive", testInactive),
+                ("testReadComplete", testReadComplete),
+                ("testWritabilityChanged", testWritabilityChanged),
+                ("testUserInboundEvent", testUserInboundEvent),
+                ("testErrorCaught", testErrorCaught),
+                ("testRead", testRead),
+           ]
+   }
+}
+
diff --git a/Tests/NIOExtrasTests/DebugInboundEventsHandlerTest.swift b/Tests/NIOExtrasTests/DebugInboundEventsHandlerTest.swift
new file mode 100644
index 0000000..1b9be4e
--- /dev/null
+++ b/Tests/NIOExtrasTests/DebugInboundEventsHandlerTest.swift
@@ -0,0 +1,124 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import XCTest
+import NIO
+import NIOExtras
+
+class DebugInboundEventsHandlerTest: XCTestCase {
+    
+    private var channel: EmbeddedChannel!
+    private var lastEvent: DebugInboundEventsHandler.Event!
+    private var handlerUnderTest: DebugInboundEventsHandler!
+    
+    override func setUp() {
+        super.setUp()
+        channel = EmbeddedChannel()
+        handlerUnderTest = DebugInboundEventsHandler { event, _ in
+            self.lastEvent = event
+        }
+        try? channel.pipeline.add(handler: handlerUnderTest).wait()
+    }
+    
+    override func tearDown() {
+        channel = nil
+        lastEvent = nil
+        handlerUnderTest = nil
+        super.tearDown()
+    }
+    
+    func testRegistered() {
+        channel.pipeline.register(promise: nil)
+        XCTAssertEqual(lastEvent, .registered)
+    }
+    
+    func testUnregistered() {
+        channel.pipeline.fireChannelUnregistered()
+        XCTAssertEqual(lastEvent, .unregistered)
+    }
+    
+    func testActive() {
+        channel.pipeline.fireChannelActive()
+        XCTAssertEqual(lastEvent, .active)
+    }
+    
+    func testInactive() {
+        channel.pipeline.fireChannelInactive()
+        XCTAssertEqual(lastEvent, .inactive)
+    }
+    
+    func testReadComplete() {
+        channel.pipeline.fireChannelReadComplete()
+        XCTAssertEqual(lastEvent, .readComplete)
+    }
+    
+    func testWritabilityChanged() {
+        channel.pipeline.fireChannelWritabilityChanged()
+        XCTAssertEqual(lastEvent, .writabilityChanged(isWritable: true))
+    }
+    
+    func testUserInboundEvent() {
+        let eventString = "new user inbound event"
+        channel.pipeline.fireUserInboundEventTriggered(eventString)
+        XCTAssertEqual(lastEvent, .userInboundEventTriggered(event: eventString))
+    }
+    
+    func testErrorCaught() {
+        struct E: Error {
+            var localizedDescription: String {
+                return "desc"
+            }
+        }
+        let error = E()
+        channel.pipeline.fireErrorCaught(error)
+        XCTAssertEqual(lastEvent, .errorCaught(error))
+    }
+    
+    func testRead() {
+        let messageString = "message"
+        var expectedBuffer = ByteBufferAllocator().buffer(capacity: messageString.count)
+        expectedBuffer.set(string: messageString, at: 0)
+        let nioAny = NIOAny(expectedBuffer)
+        channel.pipeline.fireChannelRead(nioAny)
+        XCTAssertEqual(lastEvent, .read(data: nioAny))
+    }
+    
+}
+
+extension DebugInboundEventsHandler.Event: Equatable {
+    public static func == (lhs: DebugInboundEventsHandler.Event, rhs: DebugInboundEventsHandler.Event) -> Bool {
+        switch (lhs, rhs) {
+        case (.registered, .registered):
+            return true
+        case (.unregistered, .unregistered):
+            return true
+        case (.active, .active):
+            return true
+        case (.inactive, .inactive):
+            return true
+        case (.read(let data1), .read(let data2)):
+            return "\(data1)" == "\(data2)"
+        case (.readComplete, .readComplete):
+            return true
+        case (.writabilityChanged(let isWritable1), .writabilityChanged(let isWritable2)):
+            return isWritable1 == isWritable2
+        case (.userInboundEventTriggered(let event1), .userInboundEventTriggered(let event2)):
+            return event1 as! String == event2 as! String
+        case (.errorCaught(let error1), .errorCaught(let error2)):
+            return error1.localizedDescription == error2.localizedDescription
+        default:
+            return false
+        }
+    }
+}
diff --git a/Tests/NIOExtrasTests/DebugOutboundEventsHandlerTest+XCTest.swift b/Tests/NIOExtrasTests/DebugOutboundEventsHandlerTest+XCTest.swift
new file mode 100644
index 0000000..5646f67
--- /dev/null
+++ b/Tests/NIOExtrasTests/DebugOutboundEventsHandlerTest+XCTest.swift
@@ -0,0 +1,40 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+//
+// DebugOutboundEventsHandlerTest+XCTest.swift
+//
+import XCTest
+
+///
+/// NOTE: This file was generated by generate_linux_tests.rb
+///
+/// Do NOT edit this file directly as it will be regenerated automatically when needed.
+///
+
+extension DebugOutboundEventsHandlerTest {
+
+   static var allTests : [(String, (DebugOutboundEventsHandlerTest) -> () throws -> Void)] {
+      return [
+                ("testRegister", testRegister),
+                ("testBind", testBind),
+                ("testConnect", testConnect),
+                ("testWrite", testWrite),
+                ("testFlush", testFlush),
+                ("testRead", testRead),
+                ("testClose", testClose),
+                ("testTriggerUserOutboundEvent", testTriggerUserOutboundEvent),
+           ]
+   }
+}
+
diff --git a/Tests/NIOExtrasTests/DebugOutboundEventsHandlerTest.swift b/Tests/NIOExtrasTests/DebugOutboundEventsHandlerTest.swift
new file mode 100644
index 0000000..134c579
--- /dev/null
+++ b/Tests/NIOExtrasTests/DebugOutboundEventsHandlerTest.swift
@@ -0,0 +1,112 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import XCTest
+import NIO
+import NIOExtras
+
+class DebugOutboundEventsHandlerTest: XCTestCase {
+
+    private var channel: EmbeddedChannel!
+    private var lastEvent: DebugOutboundEventsHandler.Event!
+    private var handlerUnderTest: DebugOutboundEventsHandler!
+    
+    override func setUp() {
+        super.setUp()
+        channel = EmbeddedChannel()
+        handlerUnderTest = DebugOutboundEventsHandler { event, _ in
+            self.lastEvent = event
+        }
+        try? channel.pipeline.add(handler: handlerUnderTest).wait()
+    }
+    
+    override func tearDown() {
+        channel = nil
+        lastEvent = nil
+        handlerUnderTest = nil
+        super.tearDown()
+    }
+
+    func testRegister() {
+        channel.pipeline.register(promise: nil)
+        XCTAssertEqual(lastEvent, .register)
+    }
+    
+    func testBind() throws {
+        let address = try SocketAddress(unixDomainSocketPath: "path")
+        channel.bind(to: address, promise: nil)
+        XCTAssertEqual(lastEvent, .bind(address: address))
+    }
+    
+    func testConnect() throws {
+        let address = try SocketAddress(unixDomainSocketPath: "path")
+        channel.connect(to: address, promise: nil)
+        XCTAssertEqual(lastEvent, .connect(address: address))
+    }
+    
+    func testWrite() {
+        let data = NIOAny(" 1 2 3 ")
+        channel.write(data, promise: nil)
+        XCTAssertEqual(lastEvent, .write(data: data))
+    }
+    
+    func testFlush() {
+        channel.flush()
+        XCTAssertEqual(lastEvent, .flush)
+    }
+    
+    func testRead() {
+        channel.read()
+        XCTAssertEqual(lastEvent, .read)
+    }
+    
+    func testClose() {
+        channel.close(mode: .all, promise: nil)
+        XCTAssertEqual(lastEvent, .close(mode: .all))
+    }
+    
+    func testTriggerUserOutboundEvent() {
+        let event = "user event"
+        channel.triggerUserOutboundEvent(event, promise: nil)
+        XCTAssertEqual(lastEvent, .triggerUserOutboundEvent(event: event))
+    }
+
+}
+
+extension DebugOutboundEventsHandler.Event: Equatable {
+    public static func == (lhs: DebugOutboundEventsHandler.Event, rhs: DebugOutboundEventsHandler.Event) -> Bool {
+        switch (lhs, rhs) {
+        case (.register, .register):
+            return true
+        case (.bind(let address1), .bind(let address2)):
+            return address1 == address2
+        case (.connect(let address1), .connect(let address2)):
+            return address1 == address2
+        case (.write(let data1), .write(let data2)):
+            return "\(data1)" == "\(data2)"
+        case (.flush, .flush):
+            return true
+        case (.read, .read):
+            return true
+        case (.close(let mode1), .close(let mode2)):
+            return mode1 == mode2
+        case (.triggerUserOutboundEvent(let event1), .triggerUserOutboundEvent(let event2)):
+            return "\(event1)" == "\(event2)"
+        default:
+            return false
+        }
+    }
+    
+}
+