Skip to content

Commit

Permalink
feat(decentralized-logging): extend base controller class with decent…
Browse files Browse the repository at this point in the history
…ralized logging functionality; add corresponding test and a new section in developer guide
  • Loading branch information
lukasz-zet committed Jan 20, 2021
1 parent 882a298 commit 4e333e3
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 1 deletion.
7 changes: 6 additions & 1 deletion CoatySwift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
9E3C8FC124B488EC002159AA /* IoStateEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E3C8FC024B488EC002159AA /* IoStateEvent.swift */; };
9E3C8FC324B488F7002159AA /* IoValueEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E3C8FC224B488F7002159AA /* IoValueEvent.swift */; };
9E55CDA4255AA3BD00F411DA /* ObjectLifecycleController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E55CDA3255AA3BD00F411DA /* ObjectLifecycleController.swift */; };
9E58734525B8608A00BD0193 /* DecentralizedLoggingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E58734425B8608A00BD0193 /* DecentralizedLoggingTest.swift */; };
9E73C8FF24A4CCE70010AA20 /* AssociateEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E73C8FE24A4CCE70010AA20 /* AssociateEvent.swift */; };
9E73C91024A4DE640010AA20 /* BasicIoRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E73C90F24A4DE640010AA20 /* BasicIoRouter.swift */; };
9E73C91224A4DE740010AA20 /* IoActorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E73C91124A4DE740010AA20 /* IoActorController.swift */; };
Expand Down Expand Up @@ -126,6 +127,7 @@
9E3C8FC024B488EC002159AA /* IoStateEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IoStateEvent.swift; sourceTree = "<group>"; };
9E3C8FC224B488F7002159AA /* IoValueEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IoValueEvent.swift; sourceTree = "<group>"; };
9E55CDA3255AA3BD00F411DA /* ObjectLifecycleController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectLifecycleController.swift; sourceTree = "<group>"; };
9E58734425B8608A00BD0193 /* DecentralizedLoggingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecentralizedLoggingTest.swift; sourceTree = "<group>"; };
9E73C8FE24A4CCE70010AA20 /* AssociateEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociateEvent.swift; sourceTree = "<group>"; };
9E73C90F24A4DE640010AA20 /* BasicIoRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicIoRouter.swift; sourceTree = "<group>"; };
9E73C91124A4DE740010AA20 /* IoActorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IoActorController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -299,7 +301,6 @@
9E774E01249B74DD00EF888A /* CoatySwift */,
9E774E0F249B752100EF888A /* CoatySwiftExample */,
9E774E27249B75B500EF888A /* CoatySwiftTests */,
9E774E00249B74DD00EF888A /* Products */,
1E4C3100FB66DDBCB65CBBC9 /* Pods */,
E38B4147A62870790AEB1CE3 /* Frameworks */,
);
Expand All @@ -313,6 +314,7 @@
9E774E26249B75B500EF888A /* CoatySwiftTests.xctest */,
);
name = Products;
path = ..;
sourceTree = "<group>";
};
9E774E01249B74DD00EF888A /* CoatySwift */ = {
Expand Down Expand Up @@ -350,6 +352,8 @@
9E3033F2255D490500021821 /* ObjectLifecycleControllerTests.swift */,
9EB36BB4259213000057C65E /* SensorThingsTests.swift */,
9EB36BBF259213380057C65E /* SensorThingsMocks.swift */,
9E58734425B8608A00BD0193 /* DecentralizedLoggingTest.swift */,
9E774E00249B74DD00EF888A /* Products */,
9E774E2A249B75B500EF888A /* Info.plist */,
);
path = CoatySwiftTests;
Expand Down Expand Up @@ -913,6 +917,7 @@
files = (
9E3033F3255D490500021821 /* ObjectLifecycleControllerTests.swift in Sources */,
9EB36BB5259213000057C65E /* SensorThingsTests.swift in Sources */,
9E58734525B8608A00BD0193 /* DecentralizedLoggingTest.swift in Sources */,
9EB36BC0259213380057C65E /* SensorThingsMocks.swift in Sources */,
9E774E29249B75B500EF888A /* CoatySwiftTests.swift in Sources */,
9E774EC3249B9D6200EF888A /* ObjectMatcherTests.swift in Sources */,
Expand Down
90 changes: 90 additions & 0 deletions CoatySwift/Classes/Runtime/Controller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,57 @@ open class Controller {
self.communicationManager = container.communicationManager
}

// MARK: - Distributed logging.

/// Advertise a Log object for debugging purposes.
///
/// - Parameters:
/// - message: a debug message
/// - tags: any number of log tags
public func logDebug(message: String, tags: [String]...) {
self._log(logLevel: .debug, message: message, tags: tags.reduce([], +))
}

/// Advertise an informational Log object.
///
/// - Parameters:
/// - message: an informational message
/// - tags: any number of log tags
public func logInfo(message: String, tags: [String]...) {
self._log(logLevel: .info, message: message, tags: tags.reduce([], +))
}

/// Advertise a Log object for a warning.
///
/// - Parameters:
/// - message: a warning message
/// - tags: any number of log tags
public func logWarning(message: String, tags: [String]...) {
self._log(logLevel: .warning, message: message, tags: tags.reduce([], +))
}

/// Advertise a Log object for an error.
///
/// - Parameters:
/// - error: a error (object)
/// - message: additional error message
/// - tags: any number of log tags
public func logError(error: Any, message: String, tags: [String]...) {
let msg = "\(message): \(error)"
self._log(logLevel: .error, message: msg, tags: tags.reduce([], +))
}

/// Advertise a Log object for a fatal error.
///
/// - Parameters:
/// - error: an error (object)
/// - message: additional error message
/// - tags: any number of log tags
public func logFatal(error: Any, message: String, tags: [String]...) {
let msg = "\(message): \(error)"
self._log(logLevel: .fatal, message: msg, tags: tags.reduce([], +))
}

/// Called when the container has completely set up and injected all
/// dependency components, including all its controllers.
///
Expand Down Expand Up @@ -84,5 +135,44 @@ open class Controller {
deinit {
onDispose()
}

// MARK: - Utility methods for distributed logging functionality.

/// Whenever one of the controller's log methods (e.g. `logDebug`, `logInfo`,
/// `logWarning`, `logError`, `logFatal`) is called by application code, the
/// controller creates a Log object with appropriate property values and
/// passes it to this method before advertising it.
///
/// You can override this method to additionally set certain properties (such
/// as `LogHost.hostname` or `Log.logLabels`). Ensure that
/// `super.extendLogObject` is called in your override. The base method does
/// nothing.
///
/// - Parameter log: log object to be extended before being advertised
open func extendLogObject(log: Log) { }

private func _log(logLevel: LogLevel, message: String, tags: [String]) {
let agentInfo = self.runtime.commonOptions?.agentInfo
let pid = Double(ProcessInfo.processInfo.processIdentifier)

let hostInfo = LogHost(agentInfo: agentInfo,
pid: pid,
hostname: nil,
userAgent: nil) // always nil, because swift does not run in a browser

let log = Log(logLevel: logLevel,
logMessage: message,
logDate: CoatyTimeInterval.toLocalIsoString(date: Date(), includeMilis: true),
name: "\(self.registeredName)",
objectType: Log.objectType,
objectId: .init(),
logTags: tags,
logLabels: nil,
logHost: hostInfo)

self.extendLogObject(log: log)

try? self.communicationManager.publishAdvertise(AdvertiseEvent.with(object: log))
}

}
101 changes: 101 additions & 0 deletions CoatySwiftTests/DecentralizedLoggingTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) 2021 Siemens AG. Licensed under the MIT License.
//
// DecentralizedLoggingTest.swift
// CoatySwift

import XCTest
import CoatySwift

class DecentralizedLoggingTest: XCTestCase {

/// NOTE: Please make sure that a MQTT broker is running on localhost on port 1883 before running.
func testExample() throws {
let components1 = Components(controllers: ["LogCreateorController": LogCreatorController.self],
objectTypes: [])
let communication1 = CommunicationOptions(namespace: "Logging Test",
mqttClientOptions: MQTTClientOptions(host: "localhost",
port: UInt16(1883)),
shouldAutoStart: false)
let configuration1 = Configuration(communication: communication1)
let coatyContainer1 = Container.resolve(components: components1,
configuration: configuration1)

let components2 = Components(controllers: ["LogReceiverController": LogReceiverController.self],
objectTypes: [])
let communication2 = CommunicationOptions(namespace: "Logging Test",
mqttClientOptions: MQTTClientOptions(host: "localhost",
port: UInt16(1883)),
shouldAutoStart: false)
let configuration2 = Configuration(communication: communication2)
let coatyContainer2 = Container.resolve(components: components2,
configuration: configuration2)

// Start both coaty agents
coatyContainer1.communicationManager?.start()
coatyContainer2.communicationManager?.start()

guard let receiverController = coatyContainer2.getController(name: "LogReceiverController") as? LogReceiverController else {
return
}

// Introduce a 5 seconds waiting time to give the infrastructure time to log everything.
let exp = expectation(description: "Test after 5 seconds")
let result = XCTWaiter.wait(for: [exp], timeout: 5.0)
if result == XCTWaiter.Result.timedOut {
// Check if all log event have been received
// Following loop can be used to expect each log object to check for decoding problems.
// receiverController.logStorage.forEach { log in
// print(log.logTags)
// print(log.logLabels)
// print(log.logHost)
// }
XCTAssertTrue(receiverController.logStorage.count == 50)
} else {
XCTFail("Delay interrupted")
}

// Shutdown both containers explicitly
coatyContainer1.shutdown()
coatyContainer2.shutdown()
}
}

class LogCreatorController: Controller {
override func extendLogObject(log: Log) {
log.logLabels = [
"nonce": Int.random(in: 0...10000)
]
}

override func onCommunicationManagerStarting() {
self.publishMultipleLogs()
}

/// Publishes 50 log objects in total
private func publishMultipleLogs() {
for _ in 0...9 {
self.logInfo(message: "Info Log", tags: ["tag1", "tag2"])
self.logDebug(message: "Debug Log", tags: ["tag1", "tag2"])
self.logWarning(message: "Warning Log", tags: ["tag1", "tag2"])
self.logError(error: CoatySwiftError.RuntimeError("Random error"), message: "Error Log", tags: ["tag1", "tag2"])
self.logFatal(error: CoatySwiftError.RuntimeError("Random fatal error"), message: "Fatal Log", tags: ["tag1", "tag2"])
}
}
}

class LogReceiverController: Controller {
public var logStorage: [Log] = []

override func onInit() {
self.logStorage = .init()
}

override func onCommunicationManagerStarting() {
_ = self.communicationManager.observeAdvertise(withCoreType: .Log).subscribe(onNext: { event in
guard let logObject = event.data.object as? Log else {
fatalError("Expected a Log object, but got something different. Stopping")
}
self.logStorage.append(logObject)
})
}
}
62 changes: 62 additions & 0 deletions docs/man/developer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ this guide.
- [NormalStateActorAgent](#normalstateactoragent)
- [EmergencyStateActorAgent](#emergencystateactoragent)
- [Sensor Things API](#sensor-things-api)
- [Decentralized Logging](#decentralized-logging)
- [Bootstrapping a Coaty container](#bootstrapping-a-coaty-container)
- [Creating controllers](#creating-controllers)
- [Custom object types](#custom-object-types)
Expand Down Expand Up @@ -493,6 +494,67 @@ If you want to learn how to use Sensor Things API implementation in Coaty Swift
please refer to [Coaty Swift Sensor Things
Guide](https://github.com/coatyio/coaty-swift/man/sensor-things-guide.md).

## Decentralized Logging

The Coaty framework provides the object type `Log` for decentralized structured
logging of any kind of informational events in your Coaty agents, such as
errors, warnings, system and application-specific messages. Log objects are
usually published to interested parties using an Advertise event. These log
objects can then be collected and ingested into external processing pipelines
such as the [ELK Stack](https://www.elastic.co/elk-stack).

A controller can publish a log object by creating and advertising a `Log`
object. You can specify the level of logging (debug, info, warning, error,
fatal), the message to log, its creation timestamp, and other optional
information about the host environment in which this log object is created. You
can also extend the `Log` object with custom property-value pairs.

You can also specify log tags as an array of string values in the `Log.logTags`
property. Tags are used to categorize or filter log output. Agents may introduce
specific tags, such as "service" or "app", usually defined at design time.

You can also specify log labels as a set of key-value label pairs in the
`logLabels` property. It can be used to add context-specific information to a
log object. For example, labels are useful in providing multi-dimensional data
along a log entry to be exploited by external logging services, such as
Prometheus.

For convenience, the base `Controller` class provides methods for
publishing/advertising log objects:

* `logDebug`
* `logInfo`
* `logWarning`
* `logError`
* `logFatal`

The base `Controller` class also defines a protected method
`extendLogObject(log: Log)` which is invoked by the controller whenever one of
the above log methods is called. The controller first creates a `Log` object
with appropriate property values and passes it to this method before advertising
it. You can overwrite this method to additionally set certain properties (such
as `Log.hostname` or `Log.logLabels`). For example, a Node.js agent could add
the hostname and other host characteristics to the `Log` object like this:

```swift
override func extendLogObject(log: Log) {
log.logHost.hostname = hostname;
log.logLabels = [
"operatingState": self.communicationManager.operatingState
]
}
```

To collect all log objects advertised by agent controllers, implement a logging
controller that observes Advertise events on the core type `Log`. Take a look at
the Hello World example of the Coaty framework to see how this is implemented in
detail.

Future versions of the framework could include predefined logging controllers
that collect `Log` entries, store them persistently; output them to file or
console, and provide a query interface for analyzing and visualizing log entries
by external tools.

## Bootstrapping a Coaty container

In order to get your Coaty application running, you will have to set up the
Expand Down

0 comments on commit 4e333e3

Please sign in to comment.