3

首先,重要的是要知道,OSLogStore 直到4 个月前才在 iOS 中运行。由于那是最近的事,而且文档如此稀少,一年前可能是真的,今天可能不是真的。

这是我的问题的一些背景(几乎是从这个 reddit 帖子中偷来的):

我开发的一个开源应用程序有大约 1000 个用户,用户不时会报告一些奇怪的行为。

该应用程序使用 [ Logger ] 记录重要事件...是否有可以使用的远程日志记录服务,以便我可以获取此信息并为用户解决问题?

我只关心统一日志系统是否有解决此问题的方法。假设我能够以“奇怪的行为”与用户交谈,而且他们是非技术性的。

我得到了一些提示,OSLogStore 可能是一种获取远程日志的方法,但是官方文档太少了,我说不出来。具体来说,init(url:)看起来很有趣,但也许它只接受file://协议或其他东西。

日志记录文档说它可以用于“当您无法将调试器附加到应用程序时,例如当您在用户机器上诊断问题时”,但它没有说明如何执行此操作。

4

2 回答 2

3

看完这篇文章的讨论后,我想做一个简单的原型,看看是否可以从手机远程获取日志。为此,我稍微修改了 Steipete 的代码:我删除了一些我不需要的代码,并添加了一个按钮来触发日志的发送,名为“将日志发送给开发人员”。

然后,我创建了一个名为SendableLog转换的可编码结构OSLogEntryLog,从而可以将其转换为 JSON。在使用日志getEntries()并将它们映射到这种新类型之后,我将日志转换为 JSON,并向我在 MacBook 上运行的简单 Python 服务器上的端点(如 @DanielKaplan 建议的那样)发送 HTTP POST 请求。

Swift 代码(iOS 15 应用程序):

//
//  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()
    }
}

我有这个简单的 Python HTTP 服务器来监听传入的 POST 请求,IP 地址设置为我 MacBook 的本地 IP 地址。这与上面 Swift 代码中的 IP 地址相匹配。

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.")

当我运行应用程序并点击向开发人员发送日志按钮时,我在终端中看到以下消息:

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'}]

日志从手机成功检索,然后发送到服务器。

警告

当我(完全)关闭应用程序并重新打开它时,以前的日志就消失了!

创建日志存储 ( let logStore = try OSLogStore(scope: .currentProcessIdentifier)) 时,范围设置为.currentProcessIdentifier,这是 iOS 上唯一可用的范围。这个线程让我相信.system范围也将包括以前的日志,但系统范围在 iOS 上不可用。

于 2021-12-23T13:08:26.663 回答
2

回复:@RobNapier 对原始帖子的评论说:“唯一的问题是您是否可以从远程设备上注销……即使这也很棘手。” 我开始认为OSLogStore只获取本地日志,但这使您可以它们发送到任何地方,或者对它们做任何您想做的事情,真的。

现在OSLogStore可以在 iOS 上运行,您可以在应用程序中放置一个标有“向开发人员发送日志”的按钮,单击该按钮会将日志发送到服务器上的自定义端点。这需要两个步骤:

  1. 获取本地日志。

    您链接的文章的另一部分说:

    通过 OSLogStore,Apple 添加了一个 API 以编程方式访问日志存档。它允许访问 OSLogEntryLog,其中包含您可能需要的所有日志信息。...让我们看看它是如何工作的:

     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. 将它们发送到您服务器上的自定义端点。在您的代码库中使用该功能,我认为您可以像这样使用它:

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

让我停下来的一个部分是@RobNapier 说“从远程设备注销......非常棘手”。这让我觉得我缺少一些东西。希望@RobNapier 能指出我的想法中的缺陷。

于 2021-12-22T22:43:36.720 回答