iOS — How to use CocoaLumberjack for Advanced Logging — Part 2

Explore the endless possibilities of effectively writing logs to AWS Cloudwatch and sending them to the server via API.
Jul 13 2023 · 7 min read

Background

Welcome back to the second part of our series on setting up logging correctly with CocoaLumberjack in iOS.

In the previous blog, we covered the basics of CocoaLumberjack and logging into the console and file, and its use with Crashlytics.

If you have missed the previous blog then please read it before starting reading this advanced version of it,

I already explained CocoaLumberjeck briefly in the previous blog, So let’s move forward quickly by skipping details.

Sponsored

Develop a growth mindset that sees failures as opportunities for growth with Justly today.

Introduction

Logging is an essential part of app development, providing valuable insights for debugging, analysis, and support purposes. CocoaLumberjack is a popular logging framework that offers a range of powerful features to customize and manage logs effectively.

Using the advanced features will enable us to take our logging to the next level, providing enhanced readability, efficient log management, and centralized log analysis.

In this blog, we will learn how to zip the log files and upload them to the server, and send logs to the AWS CloudWatch.

Let’s dive in!

Zip log file and Upload it to the server

Archiving and sharing logs can be essential for debugging or analysis purposes.

CocoaLumberjack, in combination with the SSZipArchive library, allows us to zip log files and upload them.

  1. Add the SSZipArchive library to your project using CocoaPods or manually.
  2. Identify the log files to be zipped. For example, suppose you have a directory named Logs that contains log files.

If we consider our previous blog example, we have added the DDFileLoggerProvider.swift file to customize the logging methods.

Let’s add new methods in the same class.

First, we have to create a directory where we can store the log file.

func createZipDirectory() {
    let path = NSTemporaryDirectory() + "/Logs"
    if !FileManager.default.fileExists(atPath: path) {
        do {
            try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
        } catch {
            LogE("DDFileLoger : Unable to create directory at:\(path), error:\(error)")
        }
    }
}

Then let’s zip the log file at the predefined location.

func zipLogFiles() -> URL {
    let df = DateFormatter()
    df.dateFormat = "yyyy-MM-dd-hh-mm-ss"
    let now = df.string(from: Date())
    let destination = FileManager.default.temporaryDirectory.appendingPathComponent("Logs/\(now).zip")
    SSZipArchive.createZipFile(atPath: destination.path, withContentsOfDirectory: logFileManager.logsDirectory)
    return destination
}

After zipping the log files, you can choose how to upload them, such as using an API or SDK specific to your desired destination, like Amazon S3. For instance, using the AWS SDK for iOS.

Example:

Here, our goal is to allow users to send feedback reports when they encounter bugs, crashes, or any other abnormalities in the app.

To achieve this, we need to collect all the logs from the app and combine them into a single file, which will be sent to the developer using an API or SDK such as Amazon S3.

In this example, we’ll focus on sending the logs to the server via an API call. Let’s assume we have a “Report Problem” screen with a submit button. When the user taps on the submit button, we need to follow these steps:

Note: I’m adding changes to the same example that we created for the previous blog.

  • Let’s first add one logging object in our ViewModel class.
let logger = DDFileLoggerProvider().provideLogger()
  • As a precaution, we should remove previously created zip files to ensure we start with a fresh file.
func removeAllZipLogs() {
    let fileManager = FileManager.default

    let logsDir = fileManager.temporaryDirectory.appendingPathComponent("Logs")
    do {
        let fileURLs = try fileManager.contentsOfDirectory(at: logsDir,
                                                               includingPropertiesForKeys: nil,
                                                               options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants])
        try fileURLs.forEach({ fileURL in
            if fileURL.pathExtension == "zip" {
               try FileManager.default.removeItem(at: fileURL)
            }
        })
    } catch {
        LogE("DDFileLogger : remove all zip error \(error)")
    }
}
  • Let’s combine everything in a single place.
func submitReport() {
    logger.removeAllZipLogs() // remove previously created zip files
    logger.createZipDirectory() // create a zip directory
    let logFilePath = logger.zipLogs() // create a fresh zip file
    .
    .
    . // Upload this log file to a server via an API call.
}

With this log file upload, if we want to send any device-related data like device version, device name, system’s current version, device type, etc then also we can send them with this log file to the server.

Upload logs to AWS CloudWatch

Integrating CocoaLumberjack with AWS CloudWatch allows us to centralize log management and gain insights into our application’s behavior.

To upload logs to AWS CloudWatch, the following steps need to be followed:

  1. Set up an AWS account and create a new CloudWatch Log Group specifically for the mobile app logs.
  2. Configure the app to use the AWS SDK for iOS and integrate it into the project.
  3. Implement the necessary code to authenticate the app with AWS using appropriate AWS credentials. This typically involves generating an access key and secret key for the app in the AWS Identity and Access Management (IAM) console.
  4. Create a custom log formatter to format logs according to the AWS CloudWatch Logs format. This ensures that logs are properly parsed and categorized in CloudWatch.
    — For this, we can also use the MyLogFormatter class which we already have defined in our previous blog.
  5. Use the AWS SDK to create a log stream within the designated CloudWatch Log Group for each user or session. This allows us to isolate and manage logs based on specific contexts.
  6. Whenever you log a message in the app, send it to CloudWatch using the appropriate AWS SDK method. This will push the log entry to the corresponding log stream in CloudWatch.

Example:

To use AWS CloudWatch for logging, we would typically need to specify the log group and stream name where the logs will be sent. This information helps organize and categorize the logs within AWS CloudWatch.

Create a default service configuration by adding the following code snippet in the application:didFinishLaunchingWithOptions: application delegate method.

// Set up AWS credentials
let credentialsProvider = AWSStaticCredentialsProvider(accessKey: "YOUR_AWS_ACCESS_KEY", secretKey: "YOUR_AWS_SECRET_KEY")
let configuration = AWSServiceConfiguration(region: .APSouth1, credentialsProvider: credentialsProvider)
 
// Register the configuration with AWSLogs
AWSServiceManager.default().defaultServiceConfiguration = configuration

For the logging, if you want to change the log level to define that the logs should be sent only above the set level, then you can change it like this,

AWSDDLog.sharedInstance.logLevel = .verbose

For setting up the group and stream with the cloud watch add the below code in one function and call that function in the app delegate.

// Add contants for log group and stream name
let LOG_GROUP = "YOUR_LOG_GROUP_NAME"
let LOG_STREAM = "YOUR_LOG_STREAM_NAME"

func createLogGroup() {
   // Create an instance of AWSLogs using the registered configuration
   let logsClient = AWSLogs.default()
 
   // Create a log group request
   let logGroupRequest = AWSLogsCreateLogGroupRequest()
   logGroupRequest?.logGroupName = LOG_GROUP
 
   logsClient.createLogGroup(logGroupRequest!) { error in
      if let error {
         print("Error creating log group: \(error.localizedDescription)")
      }
   }
}

func createLogStream() {
   let logsClient = AWSLogs.default()
 
   // Create a log stream request
   let logStreamRequest = AWSLogsCreateLogStreamRequest()
   logStreamRequest?.logStreamName = LOG_STREAM
 
   logsClient.createLogStream(logStreamRequest!) { error in
      if let error {
         print("Error creating log stream: \(error.localizedDescription)")
      }
   }
}

Note: Log stream is optional to create, as AWS will take the default log stream name if we do not provide it.

Now we need to add the main thing for adding logs to AWS Cloudwatch.

func createLogEvent(message: String) {

   // Create an instance of AWSLogs using the registered configuration
   // Allows us to interact with the CloudWatch Logs service.
   let logsClient = AWSLogs.default()
 
   let logEventRequest = AWSLogsPutLogEventsRequest()
   logEventRequest?.logGroupName = LOG_GROUP
   logEventRequest?.logStreamName = LOG_STREAM
 
   let event = AWSLogsInputLogEvent()
   event?.message = message
   event?.timestamp = NSNumber(value: Int(Date().timeIntervalSince1970 * 1000))
 
   logEventRequest?.logEvents = [event!]
 
   logsClient.putLogEvents(logEventRequest!) { response, error in
      if let error {
         print("Error sending log event: \(error.localizedDescription)")
      } else {
         // Log events sent successfully.
      }
   }
}
  1. Here, we have created an AWSLogsPutLogEventsRequest object and set the logGroupName and logStreamName properties to specify the destination log group and log stream in CloudWatch Logs
  2. We have also created an AWSLogsInputLogEvent object and set the message property to the log message you want to send. Additionally, set the timestamp property to the current timestamp in milliseconds.
  3. Assigned the log event object to the logEvents property of the request object. In this case, it's wrapped in an array as the logEvents property expects an array of log events.
  4. And at the end, we have called the putLogEvents method on the logsClient object to initiate the request to send the log event to CloudWatch Logs.

You can call this createLogEvent function whenever you need to send a log event to CloudWatch Logs. Just provide the log message as a parameter to the function.

Now, Let’s create a custom class of DDAbstractLogger for AWS logging.

class DDAWSLogger: DDAbstractLogger {
 
   override init() {
      createLogGroup()
      createLogStream()
   }
 
   override func log(message logMessage: DDLogMessage) {
      createLogEvent(message: logMessage.message)
   }
}

We have called the createLogEvent inside the log method, which will add a log to the cloud automatically whenever we get any log from the app.

Looking very simple, isn’t it?

Now, we just need to add this logger class to the CocoaLumberjack logging system with DDLog.

DDLog.add(DDAWSLogger())      // AWS logger

By following these steps, the project logs generated by CocoaLumberjack will be automatically captured and sent to the AWS cloud.

You can view and analyze these logs in the AWS dashboard.

EXTRA

CocoaLumberjack can direct logs to a file or use it as a framework that integrates with the Xcode console.

To initialize logging to files, use the following code:

let awsLogger: AWSDDFileLogger = AWSDDFileLogger() // File Logger
awsLogger.rollingFrequency = TimeInterval(60*60*24)  // 24 hours
awsLogger.logFileManager.maximumNumberOfLogFiles = 7
AWSDDLog.add(awsLogger)

In this example, we have added awsLogger to CocoaLumberjack's loggers using the AWSDDLog.add() method.

This allows the logs generated by CocoaLumberjack's log methods (e.g., DDLogDebug, DDLogInfo, etc.) to be sent to AWS CloudWatch through the configured AWS logger.

By combining the AWS logger with CocoaLumberjack’s log methods, we can log events and messages using CocoaLumberjack’s familiar syntax while also sending those logs to AWS CloudWatch for centralized storage and analysis.

To initialize logging to your Xcode console, use the following code:

AWSDDLog.add(AWSDDTTYLogger.sharedInstance) // TTY = Xcode console

That’s it.

Conclusion

Logging application data and diagnostic information to the console or a file can be very useful when debugging problems both during development and production. Having a solid logging solution in place is therefore essential.

Along with many other developers, I have created custom logging solutions for many projects, but CocoaLumberjack is an ideal replacement and it has a lot more to offer.

Remember to refer to the official CocoaLumberjack documentation for more detailed information and customization options.

Happy Logging… Happy Coding !!! 🤖🍻

Related Useful Articles


amisha-i image
Amisha Italiya
iOS developer @canopas | Sharing knowledge of iOS development


amisha-i image
Amisha Italiya
iOS developer @canopas | Sharing knowledge of iOS development

contact-footer
Say Hello!
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.