Skip to content

Commit ac4d70e

Browse files
authored
Logging on device and to Firelog (#7281)
* Restores ML Pods after M77. * Fix Package.swift * Re-add catalyst to GHA workflow. * Logging on-device and to Firelog - WIP. * Logging on-device and to Firelog - WIP. * Rename AnalyticsLogger to TelemetryLogger + minor fixes. * Add SwiftProtobuf dependency. * On-device and Firelog logging - WIP * Proto files. * Rename loggers. * Move protos to Sources. * Log model download event on device and to Firelog. * Add TODO to revisit SwiftProtobuf dependency + minor fixes. * Add SwiftProtobuf dependency to Package.swift. * TODO to revisit system info + TODO in podspec for SwiftProtobuf dependency. * Pass app to TelemetryLogger to check for stats enabled flag. * TODO to replace on device logEvent with GULLogBasic (future PR).
1 parent b1766c2 commit ac4d70e

12 files changed

+2005
-5
lines changed

FirebaseMLModelDownloader.podspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ Pod::Spec.new do |s|
3333
s.framework = 'Foundation'
3434
s.dependency 'FirebaseCore', '~> 7.0'
3535
s.dependency 'FirebaseInstallations', '~> 7.0'
36+
# TODO: Revisit this dependency
37+
s.dependency 'SwiftProtobuf', '~> 1.0'
3638

3739
s.pod_target_xcconfig = {
3840
'GCC_C_LANGUAGE_STANDARD' => 'c99',
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
import os
17+
18+
/// Enum of debug messages.
19+
// TODO: Create list of all possible messages with code - according to format.
20+
enum LoggerMessageCode {
21+
case modelDownloaded
22+
case downloadedModelMovedToURL
23+
case analyticsEventEncodeError
24+
case telemetryInitError
25+
}
26+
27+
/// On-device logger.
28+
class DeviceLogger {
29+
/// Log event on device.
30+
static func logEvent(level: OSLogType, category: OSLog, message: StaticString,
31+
messageCode: LoggerMessageCode) {
32+
// TODO: Replace with GULLogBasic.
33+
os_log(message, log: category, type: level)
34+
}
35+
}
36+
37+
/// Extension to categorize on-device logging.
38+
extension OSLog {
39+
private static let subsystem: String = {
40+
let bundleID = Bundle.main.bundleIdentifier ?? ""
41+
return "com.google.firebaseml.\(bundleID)"
42+
}()
43+
44+
/// List of logging categories.
45+
static let modelDownload = OSLog(subsystem: subsystem, category: "model-download")
46+
static let analytics = OSLog(subsystem: subsystem, category: "analytics")
47+
}

FirebaseMLModelDownloader/Sources/ModelDownloadTask.swift

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import Foundation
1616
import FirebaseCore
1717

1818
/// Possible states of model downloading.
19-
enum DownloadStatus {
19+
enum ModelDownloadStatus {
2020
case notStarted
2121
case inProgress
22-
case completed
22+
case successful
23+
case failed
2324
}
2425

2526
/// Progress and completion handlers for a model download.
@@ -49,17 +50,21 @@ class ModelDownloadTask: NSObject {
4950
/// Progress and completion handlers associated with this model download task.
5051
private let downloadHandlers: DownloadHandlers
5152
/// Keeps track of download associated with this model download task.
52-
private(set) var downloadStatus: DownloadStatus = .notStarted
53+
private(set) var downloadStatus: ModelDownloadStatus = .notStarted
5354
/// URLSession to handle model downloads.
5455
private lazy var downloadSession = URLSession(configuration: .ephemeral,
5556
delegate: self,
5657
delegateQueue: nil)
58+
/// Telemetry logger.
59+
private let telemetryLogger: TelemetryLogger?
5760

5861
init(remoteModelInfo: RemoteModelInfo, appName: String, defaults: UserDefaults,
62+
telemetryLogger: TelemetryLogger? = nil,
5963
progressHandler: DownloadHandlers.ProgressHandler? = nil,
6064
completion: @escaping DownloadHandlers.Completion) {
6165
self.remoteModelInfo = remoteModelInfo
6266
self.appName = appName
67+
self.telemetryLogger = telemetryLogger
6368
self.defaults = defaults
6469
downloadHandlers = DownloadHandlers(
6570
progressHandler: progressHandler,
@@ -92,19 +97,34 @@ extension ModelDownloadTask: URLSessionDownloadDelegate {
9297
downloadTask: URLSessionDownloadTask,
9398
didFinishDownloadingTo location: URL) {
9499
assert(downloadTask == self.downloadTask)
95-
downloadStatus = .completed
96100
let modelFileURL = ModelFileManager.getDownloadedModelFilePath(
97101
appName: appName,
98102
modelName: remoteModelInfo.name
99103
)
100104
do {
101105
try ModelFileManager.moveFile(at: location, to: modelFileURL)
102106
} catch let error as DownloadError {
107+
downloadStatus = .failed
108+
telemetryLogger?.logModelDownloadEvent(eventName: .modelDownload, status: downloadStatus)
109+
DeviceLogger.logEvent(
110+
level: .info,
111+
category: .modelDownload,
112+
message: "Unable to save downloaded remote model file.",
113+
messageCode: .modelDownloaded
114+
)
103115
DispatchQueue.main.async {
104116
self.downloadHandlers
105117
.completion(.failure(error))
106118
}
107119
} catch {
120+
downloadStatus = .failed
121+
telemetryLogger?.logModelDownloadEvent(eventName: .modelDownload, status: downloadStatus)
122+
DeviceLogger.logEvent(
123+
level: .info,
124+
category: .modelDownload,
125+
message: "Unable to save downloaded remote model file.",
126+
messageCode: .modelDownloaded
127+
)
108128
DispatchQueue.main.async {
109129
self.downloadHandlers
110130
.completion(.failure(.internalError(description: error.localizedDescription)))
@@ -117,6 +137,13 @@ extension ModelDownloadTask: URLSessionDownloadDelegate {
117137
localModelInfo.writeToDefaults(defaults, appName: appName)
118138
/// Build model from model info.
119139
let model = CustomModel(localModelInfo: localModelInfo)
140+
downloadStatus = .successful
141+
telemetryLogger?.logModelDownloadEvent(
142+
eventName: .modelDownload,
143+
status: downloadStatus,
144+
model: model
145+
)
146+
120147
DispatchQueue.main.async {
121148
self.downloadHandlers.completion(.success(model))
122149
}

FirebaseMLModelDownloader/Sources/ModelDownloader.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public class ModelDownloader {
6262
private let installations: Installations
6363
/// User defaults for model info.
6464
private let userDefaults: UserDefaults
65+
/// Telemetry logger tied to this instance of model downloader.
66+
let telemetryLogger: TelemetryLogger?
6567

6668
/// Shared dictionary mapping app name to a specific instance of model downloader.
6769
// TODO: Switch to using Firebase components.
@@ -72,6 +74,8 @@ public class ModelDownloader {
7274
appName = app.name
7375
options = app.options
7476
installations = Installations.installations(app: app)
77+
/// Respect Firebase-wide data collection setting.
78+
telemetryLogger = TelemetryLogger(app: app)
7579
userDefaults = defaults
7680

7781
NotificationCenter.default.addObserver(
@@ -268,6 +272,7 @@ extension ModelDownloader {
268272
remoteModelInfo: remoteModelInfo,
269273
appName: self.appName,
270274
defaults: self.userDefaults,
275+
telemetryLogger: self.telemetryLogger,
271276
progressHandler: progressHandler,
272277
completion: completion
273278
)
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
import FirebaseCore
17+
import GoogleDataTransport
18+
19+
/// Extension to set Firebase app info.
20+
extension SystemInfo {
21+
mutating func setAppInfo(apiKey: String?, projectID: String?) {
22+
appID = Bundle.main.bundleIdentifier ?? "unknownBundleID"
23+
// TODO: Reconsider using app version.
24+
let appVersionKey = "CFBundleShortVersionString"
25+
appVersion = Bundle.main.infoDictionary?[appVersionKey] as? String ?? "unknownAppVersion"
26+
// TODO: May also need to log SDK version.
27+
self.apiKey = apiKey ?? "unknownAPIKey"
28+
firebaseProjectID = projectID ?? "unknownProjectID"
29+
}
30+
}
31+
32+
/// Extension to set model options.
33+
extension ModelOptions {
34+
mutating func setModelOptions(model: CustomModel, isModelUpdateEnabled: Bool? = nil) {
35+
if let updateEnabled = isModelUpdateEnabled {
36+
self.isModelUpdateEnabled = updateEnabled
37+
}
38+
modelInfo.name = model.name
39+
modelInfo.hash = model.hash
40+
modelInfo.modelType = .custom
41+
}
42+
}
43+
44+
/// Extension to build model download log event.
45+
extension ModelDownloadLogEvent {
46+
mutating func setEvent(status: DownloadStatus, errorCode: ErrorCode? = nil,
47+
roughDownloadDuration: UInt64? = nil, exactDownloadDuration: UInt64? = nil,
48+
downloadFailureStatus: Int64? = nil, modelOptions: ModelOptions) {
49+
downloadStatus = status
50+
if let code = errorCode {
51+
self.errorCode = code
52+
}
53+
if let roughDuration = roughDownloadDuration {
54+
roughDownloadDurationMs = roughDuration
55+
}
56+
if let exactDuration = exactDownloadDuration {
57+
exactDownloadDurationMs = exactDuration
58+
}
59+
if let failureStatus = downloadFailureStatus {
60+
self.downloadFailureStatus = failureStatus
61+
}
62+
options = modelOptions
63+
}
64+
}
65+
66+
/// Extension to build Firebase ML log event.
67+
extension FirebaseMlLogEvent {
68+
mutating func setEvent(eventName: EventName, systemInfo: SystemInfo,
69+
modelDownloadLogEvent: ModelDownloadLogEvent) {
70+
self.eventName = eventName
71+
self.systemInfo = systemInfo
72+
self.modelDownloadLogEvent = modelDownloadLogEvent
73+
}
74+
}
75+
76+
/// Data object for Firelog event.
77+
class FBMLDataObject: NSObject, GDTCOREventDataObject {
78+
private let event: FirebaseMlLogEvent
79+
80+
init(event: FirebaseMlLogEvent) {
81+
self.event = event
82+
}
83+
84+
/// Encode Firelog event for transport.
85+
func transportBytes() -> Data {
86+
do {
87+
// TODO: Should this be binary or json serialized?
88+
let data = try event.serializedData()
89+
return data
90+
} catch {
91+
DeviceLogger.logEvent(
92+
level: .debug,
93+
category: .analytics,
94+
message: "Unable to encode Firelog event.",
95+
messageCode: .analyticsEventEncodeError
96+
)
97+
return Data()
98+
}
99+
}
100+
}
101+
102+
/// Firelog logger.
103+
class TelemetryLogger {
104+
/// Mapping ID for the log source.
105+
private let mappingID = "1326"
106+
/// Current Firebase app.
107+
private let app: FirebaseApp
108+
/// Transport for Firelog events.
109+
private let fllTransport: GDTCORTransport
110+
111+
/// Init logger, could be nil if unable to get event transport.
112+
init?(app: FirebaseApp) {
113+
self.app = app
114+
guard let fllTransport = GDTCORTransport(
115+
mappingID: mappingID,
116+
transformers: nil,
117+
target: GDTCORTarget.FLL
118+
) else {
119+
DeviceLogger.logEvent(
120+
level: .debug,
121+
category: .analytics,
122+
message: "Unable to create telemetry logger.",
123+
messageCode: .telemetryInitError
124+
)
125+
return nil
126+
}
127+
self.fllTransport = fllTransport
128+
}
129+
130+
/// Log events to Firelog.
131+
private func logModelEvent(event: FirebaseMlLogEvent) {
132+
let eventForTransport: GDTCOREvent = fllTransport.eventForTransport()
133+
eventForTransport.dataObject = FBMLDataObject(event: event)
134+
fllTransport.sendTelemetryEvent(eventForTransport)
135+
}
136+
137+
/// Log model download event to Firelog.
138+
func logModelDownloadEvent(eventName: EventName, status: ModelDownloadStatus,
139+
model: CustomModel? = nil) {
140+
guard app.isDataCollectionDefaultEnabled else { return }
141+
var modelOptions = ModelOptions()
142+
if let model = model {
143+
modelOptions.setModelOptions(model: model)
144+
}
145+
var systemInfo = SystemInfo()
146+
let apiKey = app.options.apiKey
147+
let projectID = app.options.projectID
148+
systemInfo.setAppInfo(apiKey: apiKey, projectID: projectID)
149+
150+
var modelDownloadLogEvent = ModelDownloadLogEvent()
151+
switch status {
152+
case .successful: modelDownloadLogEvent.setEvent(status: .succeeded, modelOptions: modelOptions)
153+
case .failed: modelDownloadLogEvent.setEvent(status: .failed, modelOptions: modelOptions)
154+
case .notStarted, .inProgress: break
155+
}
156+
var fbmlEvent = FirebaseMlLogEvent()
157+
fbmlEvent.setEvent(
158+
eventName: eventName,
159+
systemInfo: systemInfo,
160+
modelDownloadLogEvent: modelDownloadLogEvent
161+
)
162+
logModelEvent(event: fbmlEvent)
163+
}
164+
}

0 commit comments

Comments
 (0)