9

First, it's important to know that, OSLogStore didn't work in iOS as recently as 4 months ago. Since that's so recent and documentation is so sparse, what may have been true a year ago may not be true today.

Here's some context to my question (pretty much stolen from this reddit post):

I have about a 1000 users for an open source app I developed and every now and then users will report some odd behavior.

The app uses [Logger] to log important events ... Is there a remote logging service that can be used so that I can get this info and resolve issues for users?

I only care if the unified logging system has a solution to this problem. Let's assume I'm able to converse with the user with the "odd behavior" and they are non-technical.

I've been given some hints that OSLogStore may be a way to get remote logs, but the official documentation is so sparse, I can't tell. Specifically, that init(url:) seems interesting, but maybe it only accepts file:// protocols or something.

The logging documentation says it can be used "When you are unable to attach a debugger to the app, such as when you’re diagnosing problems on a user’s machine," but nowhere does it say how to do this.

7
  • If this is iOS 15 only, see steipete.com/posts/logging-in-swift/#update-ios-15, but you need to have done things before-hand. There's not much you're going to be able to do if your app is already in the field and you haven't added code to get what you need. Commented Dec 22, 2021 at 20:26
  • @RobNapier yep, read that whole thing before asking my question. It doesn't mention remote logging, afaict. Also, I'm fine making a new release to add this functionality. Commented Dec 22, 2021 at 21:21
  • Doesn't the "When you are unable to attach a debugger.." part refer to retrieving the logs via sysdiagnose? Commented Dec 22, 2021 at 21:36
  • 1
    I'm certain there's no remote logging (a la syslog or something like that). The only question is whether you can get logs off of remote devices (by extracting them from the log store), and even that is pretty tricky. I always just build my own custom logging engine. I never found Apple to have any interest in helping non-Apple devs get access to logs. Commented Dec 22, 2021 at 21:37
  • 1
    @BjornB. I've never heard of a way to get sysdiagnose output without users jumping through unworkable hoops. I don't think it's even possible without at least having a Mac (which most iOS app users don't). Commented Dec 22, 2021 at 21:42

3 Answers 3

4

After reading the discussion in this post, I wanted to make a simple prototype to see whether it is possible to get the logs from the phone remotely. To accomplish this, I modified Steipete's code a little: I removed some code I didn't need and added a button to trigger the sending of the logs, named "Send logs to the developers".

Then, I created a codable struct called SendableLog that converted the OSLogEntryLog, making it possible to convert it to JSON. After getting the logs using getEntries() and mapping them to this new type, I converted the logs to JSON and sent an HTTP POST request to an endpoint (as suggested by @DanielKaplan) on a simple Python server I was running on my MacBook.

The Swift code (iOS 15 application):

//
//  ContentView.swift
//  OSLogStoreTesting
//
//  Created by bbruns on 23/12/2021.
//  Based on Peter Steinberger (23.08.20): https://github.com/steipete/OSLogTest/blob/master/LoggingTest/ContentView.swift.
//
import SwiftUI
import OSLog
import Combine

let subsystem = "com.bbruns.OSLogStoreTesting"

func getLogEntries() throws -> [OSLogEntryLog] {
    let logStore = try OSLogStore(scope: .currentProcessIdentifier)
    let oneHourAgo = logStore.position(date: Date().addingTimeInterval(-3600))
    let allEntries = try logStore.getEntries(at: oneHourAgo)

    return allEntries
        .compactMap { $0 as? OSLogEntryLog }
        .filter { $0.subsystem == subsystem }
}

struct SendableLog: Codable {
    let level: Int
    let date, subsystem, category, composedMessage: String
}

func sendLogs() {
    let logs = try! getLogEntries()
    let sendLogs: [SendableLog] = logs.map({ SendableLog(level: $0.level.rawValue,
                                                                 date: "\($0.date)",
                                                                 subsystem: $0.subsystem,
                                                                 category: $0.category,
                                                                 composedMessage: $0.composedMessage) })
    
    // Convert object to JSON
    let jsonData = try? JSONEncoder().encode(sendLogs)
    
    // Send to my API
    let url = URL(string: "http://x.x.x.x:8000")! // IP address and port of Python server
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.httpBody = jsonData
    
    let session = URLSession.shared
    let task = session.dataTask(with: request) { (data, response, error) in
        if let httpResponse = response as? HTTPURLResponse {
            print(httpResponse.statusCode)
        }
    }
    task.resume()
}

struct ContentView: View {
    let logger = Logger(subsystem: subsystem, category: "main")

    var logLevels = ["Default", "Info", "Debug", "Error", "Fault"]
    @State private var selectedLogLevel = 0

    init() {
        logger.log("SwiftUI is initializing the main ContentView")
    }

    var body: some View {
        return VStack {
            Text("This is a sample project to test the new logging features of iOS 15.")
                .padding()

            Picker(selection: $selectedLogLevel, label: Text("Choose Log Level")) {
                ForEach(0 ..< logLevels.count) {
                    Text(self.logLevels[$0])
                }
            }.frame(width: 400, height: 150, alignment: .center)

            Button(action: {
                switch(selectedLogLevel) {
                case 0:
                    logger.log("Default log message")
                case 1:
                    logger.info("Info log message")
                case 2:
                    logger.debug("Debug log message")
                case 3:
                    logger.error("Error log message")
                default: // 4
                    logger.fault("Fault log message")
                }
            }) {
                Text("Log with Log Level \(logLevels[selectedLogLevel])")
            }.padding()
            
            Button(action: sendLogs) {
                Text("Send logs to developers")
            }
        }
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I have this simple Python HTTP server listening to incoming POST requests, the IP address was set to the local IP address of my MacBook. This matches the IP address in the Swift code above.

from http.server import BaseHTTPRequestHandler, HTTPServer
import json

hostName = "x.x.x.x" # IP address of server
serverPort = 8000

class MyServer(BaseHTTPRequestHandler):
    def _set_headers(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()

    def do_HEAD(self):
        self._set_headers()

    def do_POST(self):
        self._set_headers()
        print("Received POST")
        self.data_string = self.rfile.read(int(self.headers['Content-Length']))

        self.send_response(200)
        self.end_headers()

        data = json.loads(self.data_string)
        print(f"JSON received: \n\n {data}")

if __name__ == "__main__":        
    webServer = HTTPServer((hostName, serverPort), MyServer)
    print("Server started http://%s:%s" % (hostName, serverPort))

    try:
        webServer.serve_forever()
    except KeyboardInterrupt:
        pass

    webServer.server_close()
    print("Server stopped.")

When I run the app and tap the Send logs to developers button, I see the following message in my terminal:

x.x.x.x - - [23/Dec/2021 13:56:47] "POST / HTTP/1.1" 200 -
JSON received:

 [{'subsystem': 'com.bbruns.OSLogStoreTesting', 'level': 3, 'composedMessage': 'SwiftUI is initializing the main ContentView', 'category': 'main', 'date': '2021-12-23 12:56:43 +0000'}]

The logs are successfully retrieved from the phone and then sent to the server.

Caveat

When I (fully) close the app and reopen it, the previous logs are gone!

When creating the log store (let logStore = try OSLogStore(scope: .currentProcessIdentifier)) the scope is set to .currentProcessIdentifier, which is the only available scope on iOS. This thread makes me believe The .system scope would include previous logs as well, but the system scope is not available on iOS.

Sign up to request clarification or add additional context in comments.

3 Comments

Isn't there more than one way to create an OSLogStore? Maybe those other init methods could have better persistence.
@DanielKaplan There also is a init(url:) that "Creates a log store based on a log archive". I have no clue how to use this other initializer, yet.
Yep, I saw that too. This has been around for macOS for quite a while. I keep thinking there'll be a macOS article somewhere that explains how to use it in more detail.
2

re: @RobNapier's comment on the original post that says, “The only question is whether you can get logs off of remote devices ... and even that is pretty tricky.” I'm starting to think OSLogStore only gets local logs, but this enables you to send them anywhere, or do anything you want with them, really.

Now that OSLogStore works on iOS, you can put a button in your app labeled "Send logs to dev," where clicking it sends the logs to a custom endpoint on your server. That requires two steps:

  1. Get the local logs.

    Another part of the article you linked says:

    With OSLogStore, Apple added an API to access the log archive programmatically. It allows accessing OSLogEntryLog, which contains all the log information you’ll possibly ever need. ... Let’s look at how this works:

     func getLogEntries() throws -> [OSLogEntryLog] {
         let subsystem = Bundle.main.bundleIdentifier!
         // Open the log store.
         let logStore = try OSLogStore(scope: .currentProcessIdentifier)
    
         // Get all the logs from the last hour.
         let oneHourAgo = logStore.position(date: Date().addingTimeInterval(-3600))
    
         // Fetch log objects.
         let allEntries = try logStore.getEntries(at: oneHourAgo)
    
         // Filter the log to be relevant for our specific subsystem
         // and remove other elements (signposts, etc).
         return allEntries
             .compactMap { $0 as? OSLogEntryLog }
             .filter { $0.subsystem == subsystem }
     }
    
  2. Send them to a custom endpoint on your server. With that function in your code base, I think you can use it like this:

     let logs = getLogEntries();
     sendLogsToServer(deviceId, appVersion, ..., logs); // this is your implementation
    

The one part that gives me pause is @RobNapier said getting "logs off of remote devices ... is pretty tricky." That makes me think there is something I'm missing. Hopefully @RobNapier will point out the flaws in my thinking.

5 Comments

I can't remember if getEntries does what you think it's going to do. When I last looked at it, it didn't on iOS. But Peter Steinberger suggests it does if you're on iOS 15. (I've never had an 15-only app, so I've never tested that.)
@RobNapier in what sense do you think it might do something different? FWIW, if you can cast it into a OSLogEntryLog, OSLogEntryLog extends OSLogEntry, and it lets you get the message.
When I last worked with it, it wouldn't actually give you any records. Give it a try.
@RobNapier I made multiple logs. On my simulator I was able to to retrieve them with literally the code provided.
Daniel, FWIW I tried using OSLogStoreSystem instead of OSLogStoreCurrentProcessIdentifier i.e. I tried doing OSLogStore(scope: OSLogStore.Scope(rawValue: 0)!). It failed with the following error: The operation couldn’t be completed. (Foundation._GenericObjCError error 0.)
1

You could try LogDog

It is integrated as an external sdk (1-minute setup; works for Android and iOS).

Then all your logs and requests will automatically get logged to a remote web dashboard.
You receive the logs in real-time and can filter or search.

The logging on your users devices can be activated/deactivated remotely on your users devices.
Bonus feature: It also allows you to take screenshots or live capture the screen of your users devices.

This could help to debug issues that only happen on specific devices.

Note: I am the creator of LogDog

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.