diff --git a/FirebaseVertexAI/Sources/GenerateContentResponse.swift b/FirebaseVertexAI/Sources/GenerateContentResponse.swift index 31566889a0c..66dd83aec16 100644 --- a/FirebaseVertexAI/Sources/GenerateContentResponse.swift +++ b/FirebaseVertexAI/Sources/GenerateContentResponse.swift @@ -42,8 +42,10 @@ public struct GenerateContentResponse: Sendable { /// The response's content as text, if it exists. public var text: String? { guard let candidate = candidates.first else { - Logging.default - .error("[FirebaseVertexAI] Could not get text from a response that had no candidates.") + VertexLog.error( + code: .generateContentResponseNoCandidates, + "Could not get text from a response that had no candidates." + ) return nil } let textValues: [String] = candidate.content.parts.compactMap { part in @@ -53,8 +55,10 @@ public struct GenerateContentResponse: Sendable { return text } guard textValues.count > 0 else { - Logging.default - .error("[FirebaseVertexAI] Could not get a text part from the first candidate.") + VertexLog.error( + code: .generateContentResponseNoText, + "Could not get a text part from the first candidate." + ) return nil } return textValues.joined(separator: " ") @@ -330,8 +334,10 @@ extension FinishReason: Decodable { public init(from decoder: Decoder) throws { let value = try decoder.singleValueContainer().decode(String.self) guard let decodedFinishReason = FinishReason(rawValue: value) else { - Logging.default - .error("[FirebaseVertexAI] Unrecognized FinishReason with value \"\(value)\".") + VertexLog.error( + code: .generateContentResponseUnrecognizedFinishReason, + "Unrecognized FinishReason with value \"\(value)\"." + ) self = .unknown return } @@ -345,8 +351,10 @@ extension PromptFeedback.BlockReason: Decodable { public init(from decoder: Decoder) throws { let value = try decoder.singleValueContainer().decode(String.self) guard let decodedBlockReason = PromptFeedback.BlockReason(rawValue: value) else { - Logging.default - .error("[FirebaseVertexAI] Unrecognized BlockReason with value \"\(value)\".") + VertexLog.error( + code: .generateContentResponseUnrecognizedBlockReason, + "Unrecognized BlockReason with value \"\(value)\"." + ) self = .unknown return } diff --git a/FirebaseVertexAI/Sources/GenerativeAIService.swift b/FirebaseVertexAI/Sources/GenerativeAIService.swift index 3ebbf69f102..e8f7525d3fd 100644 --- a/FirebaseVertexAI/Sources/GenerativeAIService.swift +++ b/FirebaseVertexAI/Sources/GenerativeAIService.swift @@ -16,6 +16,7 @@ import FirebaseAppCheckInterop import FirebaseAuthInterop import FirebaseCore import Foundation +import os.log @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) struct GenerativeAIService { @@ -60,9 +61,15 @@ struct GenerativeAIService { // Verify the status code is 200 guard response.statusCode == 200 else { - Logging.network.error("[FirebaseVertexAI] The server responded with an error: \(response)") + VertexLog.error( + code: .loadRequestResponseError, + "The server responded with an error: \(response)" + ) if let responseString = String(data: data, encoding: .utf8) { - Logging.default.error("[FirebaseVertexAI] Response payload: \(responseString)") + VertexLog.error( + code: .loadRequestResponseErrorPayload, + "Response payload: \(responseString)" + ) } throw parseError(responseData: data) @@ -108,14 +115,19 @@ struct GenerativeAIService { // Verify the status code is 200 guard response.statusCode == 200 else { - Logging.network - .error("[FirebaseVertexAI] The server responded with an error: \(response)") + VertexLog.error( + code: .loadRequestStreamResponseError, + "The server responded with an error: \(response)" + ) var responseBody = "" for try await line in stream.lines { responseBody += line + "\n" } - Logging.default.error("[FirebaseVertexAI] Response payload: \(responseBody)") + VertexLog.error( + code: .loadRequestStreamResponseErrorPayload, + "Response payload: \(responseBody)" + ) continuation.finish(throwing: parseError(responseBody: responseBody)) return @@ -127,7 +139,7 @@ struct GenerativeAIService { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase for try await line in stream.lines { - Logging.network.debug("[FirebaseVertexAI] Stream response: \(line)") + VertexLog.debug(code: .loadRequestStreamResponseLine, "Stream response: \(line)") if line.hasPrefix("data:") { // We can assume 5 characters since it's utf-8 encoded, removing `data:`. @@ -179,8 +191,10 @@ struct GenerativeAIService { let tokenResult = await appCheck.getToken(forcingRefresh: false) urlRequest.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck") if let error = tokenResult.error { - Logging.default - .debug("[FirebaseVertexAI] Failed to fetch AppCheck token. Error: \(error)") + VertexLog.error( + code: .appCheckTokenFetchFailed, + "Failed to fetch AppCheck token. Error: \(error)" + ) } } @@ -202,10 +216,10 @@ struct GenerativeAIService { private func httpResponse(urlResponse: URLResponse) throws -> HTTPURLResponse { // Verify the status code is 200 guard let response = urlResponse as? HTTPURLResponse else { - Logging.default - .error( - "[FirebaseVertexAI] Response wasn't an HTTP response, internal error \(urlResponse)" - ) + VertexLog.error( + code: .generativeAIServiceNonHTTPResponse, + "Response wasn't an HTTP response, internal error \(urlResponse)" + ) throw NSError( domain: "com.google.generative-ai", code: -1, @@ -253,7 +267,7 @@ struct GenerativeAIService { // These errors do not produce specific GenerateContentError or CountTokensError cases. private func logRPCError(_ error: RPCError) { if error.isFirebaseMLServiceDisabledError() { - Logging.default.error(""" + VertexLog.error(code: .firebaseMLAPIDisabled, """ The Vertex AI for Firebase SDK requires the Firebase ML API `firebaseml.googleapis.com` to \ be enabled for your project. Get started in the Firebase Console \ (https://console.firebase.google.com/project/\(projectID)/genai/vertex) or verify that the \ @@ -269,9 +283,12 @@ struct GenerativeAIService { return try JSONDecoder().decode(type, from: data) } catch { if let json = String(data: data, encoding: .utf8) { - Logging.network.error("[FirebaseVertexAI] JSON response: \(json)") + VertexLog.error(code: .loadRequestParseResponseFailedJSON, "JSON response: \(json)") } - Logging.default.error("[FirebaseVertexAI] Error decoding server JSON: \(error)") + VertexLog.error( + code: .loadRequestParseResponseFailedJSONError, + "Error decoding server JSON: \(error)" + ) throw error } } @@ -297,9 +314,12 @@ struct GenerativeAIService { } private func printCURLCommand(from request: URLRequest) { + guard VertexLog.additionalLoggingEnabled() else { + return + } let command = cURLCommand(from: request) - Logging.verbose.debug(""" - [FirebaseVertexAI] Creating request with the equivalent cURL command: + os_log(.debug, log: VertexLog.logObject, """ + \(VertexLog.service) Creating request with the equivalent cURL command: ----- cURL command ----- \(command, privacy: .private) ------------------------ diff --git a/FirebaseVertexAI/Sources/GenerativeModel.swift b/FirebaseVertexAI/Sources/GenerativeModel.swift index 28d3ca4ba88..25e859aebac 100644 --- a/FirebaseVertexAI/Sources/GenerativeModel.swift +++ b/FirebaseVertexAI/Sources/GenerativeModel.swift @@ -85,23 +85,15 @@ public final class GenerativeModel { self.systemInstruction = systemInstruction self.requestOptions = requestOptions - if Logging.additionalLoggingEnabled() { - if ProcessInfo.processInfo.arguments.contains(Logging.migrationEnableArgumentKey) { - Logging.verbose.debug(""" - [FirebaseVertexAI] Verbose logging enabled with the \ - \(Logging.migrationEnableArgumentKey, privacy: .public) launch argument; please migrate to \ - the \(Logging.enableArgumentKey, privacy: .public) argument to ensure future compatibility. - """) - } else { - Logging.verbose.debug("[FirebaseVertexAI] Verbose logging enabled.") - } + if VertexLog.additionalLoggingEnabled() { + VertexLog.debug(code: .verboseLoggingEnabled, "Verbose logging enabled.") } else { - Logging.default.info(""" + VertexLog.info(code: .verboseLoggingDisabled, """ [FirebaseVertexAI] To enable additional logging, add \ - `\(Logging.enableArgumentKey, privacy: .public)` as a launch argument in Xcode. + `\(VertexLog.enableArgumentKey)` as a launch argument in Xcode. """) } - Logging.default.debug("[FirebaseVertexAI] Model \(name, privacy: .public) initialized.") + VertexLog.debug(code: .generativeModelInitialized, "Model \(name) initialized.") } /// Generates content from String and/or image inputs, given to the model as a prompt, that are diff --git a/FirebaseVertexAI/Sources/Logging.swift b/FirebaseVertexAI/Sources/Logging.swift deleted file mode 100644 index 5806ac2368a..00000000000 --- a/FirebaseVertexAI/Sources/Logging.swift +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation -import OSLog - -@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -struct Logging { - /// Subsystem that should be used for all Loggers. - static let subsystem = "com.google.firebase.vertex-ai" - - /// Default category used for most loggers, unless specialized. - static let defaultCategory = "" - - /// The argument required to enable additional logging. - static let enableArgumentKey = "-FIRDebugEnabled" - - /// The argument required to enable additional logging in the Google AI SDK; used for migration. - /// - /// To facilitate migration between the SDKs, this launch argument is also accepted to enable - /// additional logging at this time, though it is expected to be removed in the future. - static let migrationEnableArgumentKey = "-GoogleGenerativeAIDebugLogEnabled" - - // No initializer available. - @available(*, unavailable) - private init() {} - - /// The default logger that is visible for all users. Note: we shouldn't be using anything lower - /// than `.notice`. - static let `default` = Logger(subsystem: subsystem, category: defaultCategory) - - /// A non default - static let network: Logger = { - if additionalLoggingEnabled() { - return Logger(subsystem: subsystem, category: "NetworkResponse") - } else { - // Return a valid logger that's using `OSLog.disabled` as the logger, hiding everything. - return Logger(.disabled) - } - }() - - /// - static let verbose: Logger = { - if additionalLoggingEnabled() { - return Logger(subsystem: subsystem, category: defaultCategory) - } else { - // Return a valid logger that's using `OSLog.disabled` as the logger, hiding everything. - return Logger(.disabled) - } - }() - - /// Returns `true` if additional logging has been enabled via a launch argument. - static func additionalLoggingEnabled() -> Bool { - let arguments = ProcessInfo.processInfo.arguments - if arguments.contains(enableArgumentKey) || arguments.contains(migrationEnableArgumentKey) { - return true - } - return false - } -} diff --git a/FirebaseVertexAI/Sources/Safety.swift b/FirebaseVertexAI/Sources/Safety.swift index 2bea6eda5a9..a57900e7317 100644 --- a/FirebaseVertexAI/Sources/Safety.swift +++ b/FirebaseVertexAI/Sources/Safety.swift @@ -121,8 +121,10 @@ extension SafetyRating.HarmProbability: Decodable { public init(from decoder: Decoder) throws { let value = try decoder.singleValueContainer().decode(String.self) guard let decodedProbability = SafetyRating.HarmProbability(rawValue: value) else { - Logging.default - .error("[FirebaseVertexAI] Unrecognized HarmProbability with value \"\(value)\".") + VertexLog.error( + code: .generateContentResponseUnrecognizedHarmProbability, + "Unrecognized HarmProbability with value \"\(value)\"." + ) self = .unknown return } @@ -139,8 +141,10 @@ extension HarmCategory: Codable { public init(from decoder: Decoder) throws { let value = try decoder.singleValueContainer().decode(String.self) guard let decodedCategory = HarmCategory(rawValue: value) else { - Logging.default - .error("[FirebaseVertexAI] Unrecognized HarmCategory with value \"\(value)\".") + VertexLog.error( + code: .generateContentResponseUnrecognizedHarmCategory, + "Unrecognized HarmCategory with value \"\(value)\"." + ) self = .unknown return } diff --git a/FirebaseVertexAI/Sources/VertexLog.swift b/FirebaseVertexAI/Sources/VertexLog.swift new file mode 100644 index 00000000000..c69d019975f --- /dev/null +++ b/FirebaseVertexAI/Sources/VertexLog.swift @@ -0,0 +1,112 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import os.log + +@_implementationOnly import FirebaseCoreExtension + +enum VertexLog { + /// Log message codes for the Vertex AI SDK + /// + /// These codes should ideally not be re-used in order to facilitate matching error codes in + /// support requests to lines in the SDK. These codes should range between 0 and 999999 to avoid + /// being truncated in log messages. + enum MessageCode: Int { + // Logging Configuration + case verboseLoggingDisabled = 100 + case verboseLoggingEnabled = 101 + + // API Enablement Errors + case firebaseMLAPIDisabled = 200 + + // Model Configuration + case generativeModelInitialized = 1000 + + // Network Errors + case generativeAIServiceNonHTTPResponse = 2000 + case loadRequestResponseError = 2001 + case loadRequestResponseErrorPayload = 2002 + case loadRequestStreamResponseError = 2003 + case loadRequestStreamResponseErrorPayload = 2004 + + // Parsing Errors + case loadRequestParseResponseFailedJSON = 3000 + case loadRequestParseResponseFailedJSONError = 3001 + case generateContentResponseUnrecognizedFinishReason = 3002 + case generateContentResponseUnrecognizedBlockReason = 3003 + case generateContentResponseUnrecognizedBlockThreshold = 3004 + case generateContentResponseUnrecognizedHarmProbability = 3005 + case generateContentResponseUnrecognizedHarmCategory = 3006 + + // SDK State Errors + case generateContentResponseNoCandidates = 4000 + case generateContentResponseNoText = 4001 + case appCheckTokenFetchFailed = 4002 + + // SDK Debugging + case loadRequestStreamResponseLine = 5000 + } + + /// Subsystem that should be used for all Loggers. + static let subsystem = "com.google.firebase" + + /// Log identifier for the Vertex AI SDK. + /// + /// > Note: This corresponds to the `category` in `OSLog`. + static let service = "[FirebaseVertexAI]" + + /// The raw `OSLog` log object. + /// + /// > Important: This is only needed for direct `os_log` usage. + static let logObject = OSLog(subsystem: subsystem, category: service) + + /// The argument required to enable additional logging. + static let enableArgumentKey = "-FIRDebugEnabled" + + static func log(level: FirebaseLoggerLevel, code: MessageCode, _ message: String) { + let messageCode = String(format: "I-VTX%06d", code.rawValue) + FirebaseLogger.log( + level: level, + service: VertexLog.service, + code: messageCode, + message: message + ) + } + + static func error(code: MessageCode, _ message: String) { + log(level: .error, code: code, message) + } + + static func warning(code: MessageCode, _ message: String) { + log(level: .warning, code: code, message) + } + + static func notice(code: MessageCode, _ message: String) { + log(level: .notice, code: code, message) + } + + static func info(code: MessageCode, _ message: String) { + log(level: .info, code: code, message) + } + + static func debug(code: MessageCode, _ message: String) { + log(level: .debug, code: code, message) + } + + /// Returns `true` if additional logging has been enabled via a launch argument. + static func additionalLoggingEnabled() -> Bool { + return ProcessInfo.processInfo.arguments.contains(enableArgumentKey) + } +}