Integrating with Logging Platforms on iOS

4 min read

When putting your app in production, there is a good chance you want to have insight into how your app is doing on a technical level.

Jeroen L.
Jeroen L.
Published June 7, 2023
Integrating with Logging Platforms on iOS

You might ask yourself, are users reaching certain screens? What crashes happen in production? Are certain things happening as expected?

This is the area of production logging. Logging during development is straightforward. You just print things to the console. But in production, you do not have this luxury. Instead, you have to roll your logging backend or rely on a third party to provide these services. Stream’s Chat SDK exposes API enabling you to send our SDK’s log output to any destination you like.

Logging is most likely a cost center for your iOS app. As in, it will not be the deciding factor in your end users deciding to adopt your product. But you still need to have this essential piece in your implementation to support your product in production.

Stream is aware of the importance of the ability to log. But we do not know, nor can we decide what logging platform you should deploy. But to facilitate logging from our SDK into your logging backend, we supply all the essential bits and pieces you would need to connect the logging of essential events in our SDK to your logging infrastructure of choice.

It all begins with defining a custom LogDestination.

As an example of how easy this is, we will supply two examples of how to connect logging from our SDK to Sentry and DataDog.

Integrating iOS Stream SDK logging with Sentry

Integrating our SDK with Sentry is straightforward. You only need to create a SentryLogDestination and declare its usage to our SDK.

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import StreamChat import Sentry /// Basic destination for outputting messages to console. public class SentryLogDestination: BaseLogDestination { override open func write(message: String) { SentrySDK.capture(message: message) } } // Near the StreamChat SDK initialization you need to add this line: LogConfig.destinationTypes = [ConsoleLogDestination.self, SentryLogDestination.self] // Remove the `ConsoleLoDestination.self` when // you want to get rid of the logging of StreamChat logging to the console

That’s all you need to do.

Integrating iOS Stream SDK logging with Datadog

When you want to integrate with Datadog, things are a little bit more complicated due to a naming clash between StreamChat and Datadog. To make things more difficult, Datadog has a class named Datadog in their module named Datadog. This prevents untangling a naming clash by prefixing a symbol with the module you would like the compiler to take the type from.

The culprit is LogLevel, a class in Datadog and StreamChat SDKs.

Normally you would solve this by prefixing the symbol with its module.

swift
1
2
3
4
5
import Datadog Import StreamChat Datadog.LogLevel StreamChat.LogLevel

But in this case, this will not work. Datadog has a type named Datadog in their module. And this confuses the Swift compiler. The compiler now assumes you are looking for the type LogLevel within the type Datadog instead of looking for LogLevel on the top level of the module Datadog. Inconvenient, but something we can easily fix by putting some boundaries in place.

A boundary is created by separating things into two files. One file deals with everything Datadog related, and the other with everything related to StreamChat.

Let’s start with creating a small wrapper around Datadog.

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Foundation import Datadog class DatadogLogWrapper { var logger: Logger init() { self.logger = Logger.builder .sendNetworkInfo(true) .printLogsToConsole(true) .set(datadogReportingThreshold: .info) .build() } func log(level: Int, message: String, error: Error?, attributes: [String: Encodable]?) { let level = LogLevel(rawValue: level)! logger.log(level: level, message: message, error: error, attributes: attributes) } }

This is a pretty straightforward wrapping. It just takes the Datadog logger and makes a function available with an almost identical signature of the log function on the Datadog Logger type. Notice though, that the level is being passed as an integer.

Next, we need to create the StreamChat side of this solution.

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import Foundation import StreamChat private enum DatadogLogLevel: Int, Codable { case debug case info case notice case warn case error case critical } public class DataDogLogDestination: StreamChat.BaseLogDestination { var datadogLoggerWrapper = DatadogLogWrapper() /// Process the log details before outputting the log. /// - Parameter logDetails: Log details to be processed. public override func process(logDetails: LogDetails) { var extendedDetails: String = "" if showDate { extendedDetails += "\(dateFormatter.string(from: logDetails.date)) " } if showLevel { extendedDetails += "[\(String(describing: logDetails.level).uppercased())] " } if showIdentifier { extendedDetails += "[\(logDetails.loggerIdentifier)-\(identifier)] " } if showThreadName { extendedDetails += logDetails.threadName } if showFileName { let fileName = (String(describing: logDetails.fileName) as NSString).lastPathComponent extendedDetails += "[\(fileName)\(showLineNumber ? ":\(logDetails.lineNumber)" : "")] " } else if showLineNumber { extendedDetails += "[\(logDetails.lineNumber)] " } if showFunctionName { extendedDetails += "[\(logDetails.functionName)] " } let extendedMessage = "\(extendedDetails)> \(logDetails.message)" let formattedMessage = applyFormatters(logDetails: logDetails, message: extendedMessage) let dataDogLogLevel: DatadogLogLevel switch logDetails.level { case .debug: dataDogLogLevel = .debug case .info: dataDogLogLevel = .info case .warning: dataDogLogLevel = .warn case .error: dataDogLogLevel = .error } datadogLoggerWrapper.log(level: dataDogLogLevel.rawValue, message: formattedMessage, error: nil, attributes: nil) } }

This implementation is more involved because we need to pick apart the logged message before we send things into Datadog. The key is the enumeration type DatadogLogLevel. This is a pretty straight-up translation of Stream’s log level to something Datadog can understand. Be it through an intermediate integer while we call across the boundary we needed to create.

The final thing we need to do now is to ensure we configure the DataDogLogDestination close to where we initialize the StreamChat SDK.

LogConfig.destinationTypes = [ConsoleLogDestination.self, DataDogLogDestination.self]

Again, if you want to remove the default console logging performed by Stream’s SDK, remove the ConsoleLogDestination type from this line.

Conclusion

And that’s it. I hope these two examples have shown how easy it is to integrate any logging backend with the Stream Chat SwiftUI SDK. If you run into any trouble getting Stream Chat logging to show up in your logging backend, feel free to reach out. We are always happy to help where we can. You can get in touch through our support or on Twitter.

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->