diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index e0ee11a31d..5c4601077b 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -32,7 +32,9 @@ 432E73CB1D24B3D6009AD15D /* RemoteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 432E73CA1D24B3D6009AD15D /* RemoteDataManager.swift */; }; 433EA4C21D9F39C900CD78FB /* PumpIDTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 433EA4C11D9F39C900CD78FB /* PumpIDTableViewController.swift */; }; 433EA4C41D9F71C800CD78FB /* CommandResponseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 433EA4C31D9F71C800CD78FB /* CommandResponseViewController.swift */; }; + 4341F4EB1EDB92AC001C936B /* LogglyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4341F4EA1EDB92AC001C936B /* LogglyService.swift */; }; 43441A9C1EDB34810087958C /* StatusExtensionContext+LoopKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43441A9B1EDB34810087958C /* StatusExtensionContext+LoopKit.swift */; }; + 43441AA01EDB4D390087958C /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43441A9F1EDB4D390087958C /* OSLog.swift */; }; 4346D1E71C77F5FE00ABAFE3 /* ChartTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4346D1E61C77F5FE00ABAFE3 /* ChartTableViewCell.swift */; }; 4346D1F61C78501000ABAFE3 /* ChartPoint+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4346D1F51C78501000ABAFE3 /* ChartPoint+Loop.swift */; }; 434F54571D287FDB002A9274 /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434F54561D287FDB002A9274 /* NibLoadable.swift */; }; @@ -378,7 +380,9 @@ 4337615E1D52F487004A3647 /* GlucoseHUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseHUDView.swift; sourceTree = ""; }; 433EA4C11D9F39C900CD78FB /* PumpIDTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpIDTableViewController.swift; sourceTree = ""; }; 433EA4C31D9F71C800CD78FB /* CommandResponseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommandResponseViewController.swift; sourceTree = ""; }; + 4341F4EA1EDB92AC001C936B /* LogglyService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogglyService.swift; sourceTree = ""; }; 43441A9B1EDB34810087958C /* StatusExtensionContext+LoopKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StatusExtensionContext+LoopKit.swift"; sourceTree = ""; }; + 43441A9F1EDB4D390087958C /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; 4346D1E61C77F5FE00ABAFE3 /* ChartTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartTableViewCell.swift; sourceTree = ""; }; 4346D1EF1C781BEA00ABAFE3 /* SwiftCharts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftCharts.framework; path = Carthage/Build/iOS/SwiftCharts.framework; sourceTree = ""; }; 4346D1F51C78501000ABAFE3 /* ChartPoint+Loop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ChartPoint+Loop.swift"; sourceTree = ""; }; @@ -739,6 +743,7 @@ isa = PBXGroup; children = ( 438849EB1D29EC34003B3F23 /* AmplitudeService.swift */, + 4341F4EA1EDB92AC001C936B /* LogglyService.swift */, 438849ED1D2A1EBB003B3F23 /* MLabService.swift */, 438849E91D297CB6003B3F23 /* NightscoutService.swift */, 437CCADF1D285C7B0075D2C3 /* ServiceAuthentication.swift */, @@ -842,6 +847,7 @@ 43CEE6E51E56AFD400CB9116 /* NightscoutUploader.swift */, 4398973A1CD2FC2000223065 /* NSDateFormatter.swift */, 43E344A31B9E1B1C00C85C07 /* NSUserDefaults.swift */, + 43441A9F1EDB4D390087958C /* OSLog.swift */, 43BFF0CA1E466C0900FF19A9 /* StateColorPalette.swift */, 43F41C361D3BF32400C11ED6 /* UIAlertController.swift */, 43BFF0BB1E45C80600FF19A9 /* UIColor+Loop.swift */, @@ -1442,6 +1448,7 @@ 43776F901B8022E90074EA36 /* AppDelegate.swift in Sources */, 437CCADA1D284ADF0075D2C3 /* AuthenticationTableViewCell.swift in Sources */, 439BED2E1E760BC600B0AED5 /* EnliteCGMManager.swift in Sources */, + 4341F4EB1EDB92AC001C936B /* LogglyService.swift in Sources */, 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */, 43BFF0CB1E466C0900FF19A9 /* StateColorPalette.swift in Sources */, 438991691E91B571000EEF90 /* ChartPoint.swift in Sources */, @@ -1500,6 +1507,7 @@ 43F41C371D3BF32400C11ED6 /* UIAlertController.swift in Sources */, 433EA4C41D9F71C800CD78FB /* CommandResponseViewController.swift in Sources */, 434F545F1D288345002A9274 /* ShareService.swift in Sources */, + 43441AA01EDB4D390087958C /* OSLog.swift in Sources */, 43CEE6E61E56AFD400CB9116 /* NightscoutUploader.swift in Sources */, 4328E0331CFC091100E199AA /* WatchContext+LoopKit.swift in Sources */, 4F526D611DF8D9A900A04910 /* NetBasal.swift in Sources */, diff --git a/Loop/AppDelegate.swift b/Loop/AppDelegate.swift index d35f243872..bf79ce0f9e 100644 --- a/Loop/AppDelegate.swift +++ b/Loop/AppDelegate.swift @@ -23,7 +23,11 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { NotificationManager.authorize(delegate: self) - AnalyticsManager.sharedManager.application(application, didFinishLaunchingWithOptions: launchOptions) + let bundle = Bundle(for: type(of: self)) + DiagnosticLogger.shared = DiagnosticLogger(subsystem: bundle.bundleIdentifier!, version: bundle.shortVersionString) + DiagnosticLogger.shared?.forCategory("AppDelegate").info(#function) + + AnalyticsManager.shared.application(application, didFinishLaunchingWithOptions: launchOptions) if let navVC = window?.rootViewController as? UINavigationController, let statusVC = navVC.viewControllers.first as? StatusTableViewController { @@ -75,7 +79,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { let startDate = response.notification.request.content.userInfo[NotificationManager.UserInfoKey.bolusStartDate.rawValue] as? Date, startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5) { - AnalyticsManager.sharedManager.didRetryBolus() + AnalyticsManager.shared.didRetryBolus() deviceManager.enactBolus(units: units, at: startDate) { (_) in completionHandler() diff --git a/Loop/Extensions/OSLog.swift b/Loop/Extensions/OSLog.swift new file mode 100644 index 0000000000..b0902d98c7 --- /dev/null +++ b/Loop/Extensions/OSLog.swift @@ -0,0 +1,27 @@ +// +// OSLog.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import os.log + + +extension OSLog { + func debug(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .debug, args) + } + + func info(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .info, args) + } + + func error(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .error, args) + } + + private func log(_ message: StaticString, type: OSLogType, _ args: CVarArg...) { + os_log(message, log: self, type: type, args) + } +} diff --git a/Loop/Managers/AnalyticsManager.swift b/Loop/Managers/AnalyticsManager.swift index 5f48b269de..5c828c6575 100644 --- a/Loop/Managers/AnalyticsManager.swift +++ b/Loop/Managers/AnalyticsManager.swift @@ -26,7 +26,7 @@ final class AnalyticsManager { } } - static let sharedManager = AnalyticsManager() + static let shared = AnalyticsManager() // MARK: - Helpers diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 1191a4b12f..5ae758ddd1 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -23,7 +23,7 @@ final class DeviceDataManager { // MARK: - Utilities - let logger = DiagnosticLogger() + let logger = DiagnosticLogger.shared! /// Remember the launch date of the app for diagnostic reporting fileprivate let launchDate = Date() @@ -61,7 +61,7 @@ final class DeviceDataManager { } if let oldVal = oldVal, newVal - oldVal >= 0.5 { - AnalyticsManager.sharedManager.pumpBatteryWasReplaced() + AnalyticsManager.shared.pumpBatteryWasReplaced() } } } @@ -93,7 +93,7 @@ final class DeviceDataManager { case is MySentryAlertMessageBody, is MySentryAlertClearedMessageBody: break case let body: - logger.addMessage(["messageType": Int(message.messageType.rawValue), "messageBody": body.txData.hexadecimalString], toCollection: "sentryOther") + logger.forCategory("MySentry").info(["messageType": Int(message.messageType.rawValue), "messageBody": body.txData.hexadecimalString]) } default: break @@ -112,7 +112,7 @@ final class DeviceDataManager { rileyLinkManager.connectDevice(device) - AnalyticsManager.sharedManager.didChangeRileyLinkConnectionState() + AnalyticsManager.shared.didChangeRileyLinkConnectionState() } func disconnectFromRileyLink(_ device: RileyLinkDevice) { @@ -120,7 +120,7 @@ final class DeviceDataManager { rileyLinkManager.disconnectDevice(device) - AnalyticsManager.sharedManager.didChangeRileyLinkConnectionState() + AnalyticsManager.shared.didChangeRileyLinkConnectionState() if connectedPeripheralIDs.count == 0 { NotificationManager.clearPendingNotificationRequests() @@ -246,7 +246,7 @@ final class DeviceDataManager { } if newValue.unitVolume > previousVolume + 1 { - AnalyticsManager.sharedManager.reservoirWasRewound() + AnalyticsManager.shared.reservoirWasRewound() } } } diff --git a/Loop/Managers/DiagnosticLogger+LoopKit.swift b/Loop/Managers/DiagnosticLogger+LoopKit.swift index 5e610942a6..0bdf1e1717 100644 --- a/Loop/Managers/DiagnosticLogger+LoopKit.swift +++ b/Loop/Managers/DiagnosticLogger+LoopKit.swift @@ -13,16 +13,14 @@ import LoopKit extension DiagnosticLogger { func addError(_ message: String, fromSource source: String) { - let info = [ - "source": source, - "message": message, - "reportedAt": DateFormatter.ISO8601StrictDateFormatter().string(from: Date()) + let message = [ + "message": message ] - addMessage(info, toCollection: "errors") + forCategory(source).error(message) } func addError(_ message: Error, fromSource source: String) { - addError(String(describing: message), fromSource: source) + forCategory(source).error(message) } } diff --git a/Loop/Managers/DiagnosticLogger.swift b/Loop/Managers/DiagnosticLogger.swift index 84d6e707a2..e647cf21b1 100644 --- a/Loop/Managers/DiagnosticLogger.swift +++ b/Loop/Managers/DiagnosticLogger.swift @@ -7,10 +7,13 @@ // import Foundation +import os.log final class DiagnosticLogger { - private lazy var isSimulator: Bool = TARGET_OS_SIMULATOR != 0 + private let isSimulator: Bool = TARGET_OS_SIMULATOR != 0 + let subsystem: String + let version: String var mLabService: MLabService { didSet { @@ -18,23 +21,121 @@ final class DiagnosticLogger { } } - init() { + var logglyService: LogglyService { + didSet { + try! KeychainManager().setLogglyCustomerToken(logglyService.customerToken) + } + } + + let remoteLogLevel: OSLogType + + static var shared: DiagnosticLogger? + + init(subsystem: String, version: String) { + self.subsystem = subsystem + self.version = version + remoteLogLevel = isSimulator ? .fault : .info + if let (databaseName, APIKey) = KeychainManager().getMLabCredentials() { mLabService = MLabService(databaseName: databaseName, APIKey: APIKey) } else { mLabService = MLabService(databaseName: nil, APIKey: nil) } + + let customerToken = KeychainManager().getLogglyCustomerToken() + logglyService = LogglyService(customerToken: customerToken) } - func addMessage(_ message: [String: Any], toCollection collection: String) { - if !isSimulator, - let messageData = try? JSONSerialization.data(withJSONObject: message, options: []), - let task = mLabService.uploadTaskWithData(messageData, inCollection: collection) - { - task.resume() - } else { - NSLog("%@: %@", collection, message) + func forCategory(_ category: String) -> CategoryLogger { + return CategoryLogger(logger: self, category: category) + } +} + + +extension OSLogType { + fileprivate var tagName: String { + switch self { + case let t where t == .info: + return "info" + case let t where t == .debug: + return "debug" + case let t where t == .error: + return "error" + case let t where t == .fault: + return "fault" + default: + return "default" + } + } +} + + +final class CategoryLogger { + private let logger: DiagnosticLogger + let category: String + + private let systemLog: OSLog + + fileprivate init(logger: DiagnosticLogger, category: String) { + self.logger = logger + self.category = category + + systemLog = OSLog(subsystem: logger.subsystem, category: category) + } + + private func remoteLog(_ type: OSLogType, message: String) { + guard logger.remoteLogLevel.rawValue <= type.rawValue else { + return + } + + logger.logglyService.client?.send(message, tags: [type.tagName, category]) + } + + private func remoteLog(_ type: OSLogType, message: [String: Any]) { + guard logger.remoteLogLevel.rawValue <= type.rawValue else { + return } + + logger.logglyService.client?.send(message, tags: [type.tagName, category]) + + // Legacy mLab logging. To be removed. + if let messageData = try? JSONSerialization.data(withJSONObject: message, options: []) { + logger.mLabService.uploadTaskWithData(messageData, inCollection: category)?.resume() + } + } + + func debug(_ message: [String: Any]) { + systemLog.debug("%{public}@", String(describing: message)) + remoteLog(.debug, message: message) + } + + func debug(_ message: String) { + systemLog.error("%{public}@", message) + remoteLog(.debug, message: message) + } + + func info(_ message: [String: Any]) { + systemLog.info("%{public}@", String(describing: message)) + remoteLog(.info, message: message) + } + + func info(_ message: String) { + systemLog.error("%{public}@", message) + remoteLog(.info, message: message) + } + + func error(_ message: [String: Any]) { + systemLog.error("%{public}@", String(reflecting: message)) + remoteLog(.error, message: message) + } + + func error(_ message: String) { + systemLog.error("%{public}@", message) + remoteLog(.error, message: message) + } + + func error(_ error: Error) { + self.error(String(reflecting: error)) } } diff --git a/Loop/Managers/KeychainManager+Loop.swift b/Loop/Managers/KeychainManager+Loop.swift index d2e2225c48..e8200283b5 100644 --- a/Loop/Managers/KeychainManager+Loop.swift +++ b/Loop/Managers/KeychainManager+Loop.swift @@ -9,20 +9,11 @@ import Foundation -private let AmplitudeAPIKeyService = "AmplitudeAPIKey" private let DexcomShareURL = URL(string: "https://share1.dexcom.com")! private let NightscoutAccount = "NightscoutAPI" extension KeychainManager { - func setAmplitudeAPIKey(_ key: String?) throws { - try replaceGenericPassword(key, forService: AmplitudeAPIKeyService) - } - - func getAmplitudeAPIKey() -> String? { - return try? getGenericPasswordForService(AmplitudeAPIKeyService) - } - func setDexcomShareUsername(_ username: String?, password: String?) throws { let credentials: InternetCredentials? diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 9822e879a4..dd20aa0df6 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -37,7 +37,7 @@ final class LoopDataManager { unowned let delegate: LoopDataManagerDelegate - private let logger = DiagnosticLogger() + private let logger: CategoryLogger init( delegate: LoopDataManagerDelegate, @@ -50,6 +50,7 @@ final class LoopDataManager { settings: LoopSettings = UserDefaults.standard.loopSettings ?? LoopSettings() ) { self.delegate = delegate + self.logger = DiagnosticLogger.shared!.forCategory("LoopDataManager") self.lastLoopCompleted = lastLoopCompleted self.lastTempBasal = lastTempBasal self.settings = settings @@ -93,7 +94,7 @@ final class LoopDataManager { didSet { UserDefaults.standard.loopSettings = settings notify(forChange: .preferences) - AnalyticsManager.sharedManager.didChangeLoopSettings(from: oldValue, to: settings) + AnalyticsManager.shared.didChangeLoopSettings(from: oldValue, to: settings) } } @@ -160,7 +161,7 @@ final class LoopDataManager { UserDefaults.standard.insulinActionDuration = newValue if oldValue != newValue { - AnalyticsManager.sharedManager.didChangeInsulinActionDuration() + AnalyticsManager.shared.didChangeInsulinActionDuration() } } } @@ -355,7 +356,7 @@ final class LoopDataManager { self.lastLoopError = error if let error = error { - self.logger.addError(error, fromSource: "TempBasal") + self.logger.error(error) } else { self.lastLoopCompleted = Date() } @@ -412,7 +413,7 @@ final class LoopDataManager { updateGroup.enter() glucoseStore.getRecentMomentumEffect { (effects, error) -> Void in if let error = error, effects.count == 0 { - self.logger.addError(error, fromSource: "GlucoseStore") + self.logger.error(error) self.glucoseMomentumEffect = nil } else { self.glucoseMomentumEffect = effects @@ -427,7 +428,7 @@ final class LoopDataManager { carbStore.getGlucoseEffects(start: retrospectiveStart) { (result) -> Void in switch result { case .failure(let error): - self.logger.addError(error, fromSource: "CarbStore") + self.logger.error(error) self.carbEffect = nil case .success(let effects): self.carbEffect = effects @@ -450,7 +451,7 @@ final class LoopDataManager { doseStore.getGlucoseEffects(start: retrospectiveStart) { (result) -> Void in switch result { case .failure(let error): - self.logger.addError(error, fromSource: "DoseStore") + self.logger.error(error) self.insulinEffect = nil case .success(let effects): self.insulinEffect = effects @@ -465,7 +466,7 @@ final class LoopDataManager { doseStore.insulinOnBoard(at: Date()) { (result) in switch result { case .failure(let error): - self.logger.addError(error, fromSource: "DoseStore") + self.logger.error(error) self.insulinOnBoard = nil case .success(let value): self.insulinOnBoard = value @@ -480,7 +481,7 @@ final class LoopDataManager { do { try updateRetrospectiveGlucoseEffect() } catch let error { - logger.addError(error, fromSource: "RetrospectiveGlucose") + logger.error(error) } } @@ -488,7 +489,7 @@ final class LoopDataManager { do { try updatePredictedGlucoseAndRecommendedBasal() } catch let error { - logger.addError(error, fromSource: "PredictGlucose") + logger.error(error) throw error } @@ -624,13 +625,13 @@ final class LoopDataManager { didSet { NotificationManager.scheduleLoopNotRunningNotifications() - AnalyticsManager.sharedManager.loopDidSucceed() + AnalyticsManager.shared.loopDidSucceed() } } fileprivate var lastLoopError: Error? { didSet { if lastLoopError != nil { - AnalyticsManager.sharedManager.loopDidError() + AnalyticsManager.shared.loopDidError() } } } diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index 1834842d8a..eabc1b780d 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -130,7 +130,7 @@ final class WatchDataManager: NSObject, WCSessionDelegate { deviceDataManager.loopManager.addCarbEntryAndRecommendBolus(newEntry) { (result) in switch result { case .success(let recommendation): - AnalyticsManager.sharedManager.didAddCarbsFromWatch(carbEntry.value) + AnalyticsManager.shared.didAddCarbsFromWatch(carbEntry.value) completionHandler?(recommendation?.amount) case .failure(let error): self.deviceDataManager.logger.addError(error, fromSource: error is CarbStore.CarbStoreError ? "CarbStore" : "Bolus") @@ -154,7 +154,7 @@ final class WatchDataManager: NSObject, WCSessionDelegate { if let bolus = SetBolusUserInfo(rawValue: message as SetBolusUserInfo.RawValue) { self.deviceDataManager.enactBolus(units: bolus.value, at: bolus.startDate) { (error) in if error == nil { - AnalyticsManager.sharedManager.didSetBolusFromWatch(bolus.value) + AnalyticsManager.shared.didSetBolusFromWatch(bolus.value) } replyHandler([:]) diff --git a/Loop/Models/ServiceAuthentication/AmplitudeService.swift b/Loop/Models/ServiceAuthentication/AmplitudeService.swift index 63074738c4..923285eb41 100644 --- a/Loop/Models/ServiceAuthentication/AmplitudeService.swift +++ b/Loop/Models/ServiceAuthentication/AmplitudeService.swift @@ -10,7 +10,7 @@ import Foundation import Amplitude -struct AmplitudeService: ServiceAuthentication { +class AmplitudeService: ServiceAuthentication { var credentials: [ServiceCredential] let title: String = NSLocalizedString("Amplitude", comment: "The title of the Amplitude service") @@ -37,7 +37,7 @@ struct AmplitudeService: ServiceAuthentication { var isAuthorized: Bool = true - mutating func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { + func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { guard let APIKey = APIKey else { isAuthorized = false completion(false, nil) @@ -52,9 +52,23 @@ struct AmplitudeService: ServiceAuthentication { completion(true, nil) } - mutating func reset() { + func reset() { credentials[0].value = nil isAuthorized = false client = nil } } + + +private let AmplitudeAPIKeyService = "AmplitudeAPIKey" + + +extension KeychainManager { + func setAmplitudeAPIKey(_ key: String?) throws { + try replaceGenericPassword(key, forService: AmplitudeAPIKeyService) + } + + func getAmplitudeAPIKey() -> String? { + return try? getGenericPasswordForService(AmplitudeAPIKeyService) + } +} diff --git a/Loop/Models/ServiceAuthentication/LogglyService.swift b/Loop/Models/ServiceAuthentication/LogglyService.swift new file mode 100644 index 0000000000..b97e541c48 --- /dev/null +++ b/Loop/Models/ServiceAuthentication/LogglyService.swift @@ -0,0 +1,141 @@ +// +// LogglyService.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import Foundation + + +class LogglyService: ServiceAuthentication { + var credentials: [ServiceCredential] + + let title: String = NSLocalizedString("Loggly", comment: "The title of the loggly service") + + init(customerToken: String?) { + credentials = [ + ServiceCredential( + title: NSLocalizedString("Customer Token", comment: "The title of the Loggly customer token credential"), + placeholder: nil, + isSecret: false, + keyboardType: .asciiCapable, + value: customerToken + ) + ] + + verify { _, _ in } + } + + var client: LogglyClient? + + var isAuthorized: Bool = true + + var customerToken: String? { + return credentials[0].value + } + + func verify(_ completion: @escaping (Bool, Error?) -> Void) { + guard let customerToken = customerToken else { + isAuthorized = false + completion(false, nil) + return + } + + isAuthorized = true + client = LogglyClient(customerToken: customerToken) + completion(true, nil) + } + + func reset() { + credentials[0].value = nil + isAuthorized = false + client = nil + } +} + + +private let LogglyURLSessionConfiguration = "LogglyURLSessionConfiguration" +private let LogglyCustomerTokenService = "LogglyCustomerToken" + + +extension KeychainManager { + func setLogglyCustomerToken(_ token: String?) throws { + try replaceGenericPassword(token, forService: LogglyCustomerTokenService) + } + + func getLogglyCustomerToken() -> String? { + return try? getGenericPasswordForService(LogglyCustomerTokenService) + } +} + + +enum LogglyAPIEndpoint: String { + case inputs + + private var base: String { + return "https://logs-01.loggly.com/\(rawValue)/" + } + + func url(token: String, tags: [String]) -> URL { + let tags = tags.count > 0 ? tags : ["http"] + return URL(string: "\(base)\(token)/tag/\(tags.joined(separator: ","))/")! + } +} + + +extension URLSession { + fileprivate static func logglySession() -> URLSession { + let configuration = URLSessionConfiguration.ephemeral + + configuration.isDiscretionary = true + configuration.sessionSendsLaunchEvents = false + configuration.networkServiceType = .background + + return URLSession(configuration: configuration) + } + + private func inputTask(body: Data, contentType: String, token: String, tags: [String]) -> URLSessionUploadTask? { + let url = LogglyAPIEndpoint.inputs.url(token: token, tags: tags) + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue(contentType, forHTTPHeaderField: "content-type") + + return uploadTask(with: request, from: body) + } + + fileprivate func inputTask(body: String, token: String, tags: [String]) -> URLSessionUploadTask? { + guard let data = body.data(using: .utf8) else { + return nil + } + + return inputTask(body: data, contentType: "text/plain", token: token, tags: tags) + } + + fileprivate func inputTask(body: [String: Any], token: String, tags: [String]) -> URLSessionUploadTask? { + do { + let data = try JSONSerialization.data(withJSONObject: body, options: []) + return inputTask(body: data, contentType: "application/json", token: token, tags: tags) + } catch { + return nil + } + } +} + + +class LogglyClient { + let customerToken: String + let session = URLSession.logglySession() + + init(customerToken: String) { + self.customerToken = customerToken + } + + func send(_ body: String, tags: [String]) { + session.inputTask(body: body, token: customerToken, tags: tags)?.resume() + } + + func send(_ body: [String: Any], tags: [String]) { + session.inputTask(body: body, token: customerToken, tags: tags)?.resume() + } +} diff --git a/Loop/Models/ServiceAuthentication/MLabService.swift b/Loop/Models/ServiceAuthentication/MLabService.swift index 174f205b1e..0330e69e6f 100644 --- a/Loop/Models/ServiceAuthentication/MLabService.swift +++ b/Loop/Models/ServiceAuthentication/MLabService.swift @@ -12,7 +12,7 @@ import Foundation private let mLabAPIHost = URL(string: "https://api.mongolab.com/api/1/databases")! -struct MLabService: ServiceAuthentication { +class MLabService: ServiceAuthentication { var credentials: [ServiceCredential] let title: String = NSLocalizedString("mLab", comment: "The title of the mLab service") @@ -50,7 +50,7 @@ struct MLabService: ServiceAuthentication { var isAuthorized: Bool = false - mutating func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { + func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { guard let APIURL = APIURLForCollection("") else { completion(false, nil) return @@ -66,7 +66,7 @@ struct MLabService: ServiceAuthentication { }).resume() } - mutating func reset() { + func reset() { credentials[0].value = nil credentials[1].value = nil isAuthorized = false diff --git a/Loop/Models/ServiceAuthentication/NightscoutService.swift b/Loop/Models/ServiceAuthentication/NightscoutService.swift index 3c796e480b..05baa0733d 100644 --- a/Loop/Models/ServiceAuthentication/NightscoutService.swift +++ b/Loop/Models/ServiceAuthentication/NightscoutService.swift @@ -11,7 +11,7 @@ import NightscoutUploadKit // Encapsulates a Nightscout site and its authentication -struct NightscoutService: ServiceAuthentication { +class NightscoutService: ServiceAuthentication { var credentials: [ServiceCredential] let title: String = NSLocalizedString("Nightscout", comment: "The title of the Nightscout service") @@ -40,8 +40,9 @@ struct NightscoutService: ServiceAuthentication { // The uploader instance, if credentials are present private(set) var uploader: NightscoutUploader? { didSet { + let logger = DiagnosticLogger.shared?.forCategory("NightscoutService") uploader?.errorHandler = { (error: Error, context: String) -> Void in - print("Error \(error), while \(context)") + logger?.error("Error \(error), while \(context)") } } } @@ -60,7 +61,7 @@ struct NightscoutService: ServiceAuthentication { var isAuthorized: Bool = true - mutating func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { + func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { guard let siteURL = siteURL, let APISecret = APISecret else { isAuthorized = false completion(false, nil) @@ -74,7 +75,7 @@ struct NightscoutService: ServiceAuthentication { self.uploader = uploader } - mutating func reset() { + func reset() { credentials[0].value = nil credentials[1].value = nil isAuthorized = false diff --git a/Loop/Models/ServiceAuthentication/ShareService.swift b/Loop/Models/ServiceAuthentication/ShareService.swift index 93b40f9370..0c0f423aef 100644 --- a/Loop/Models/ServiceAuthentication/ShareService.swift +++ b/Loop/Models/ServiceAuthentication/ShareService.swift @@ -11,7 +11,7 @@ import ShareClient // Encapsulates the Dexcom Share client service and its authentication -struct ShareService: ServiceAuthentication { +class ShareService: ServiceAuthentication { var credentials: [ServiceCredential] let title: String = NSLocalizedString("Dexcom Share", comment: "The title of the Dexcom Share service") @@ -53,7 +53,7 @@ struct ShareService: ServiceAuthentication { var isAuthorized: Bool = false - mutating func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { + func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { guard let username = username, let password = password else { completion(false, nil) return @@ -66,7 +66,7 @@ struct ShareService: ServiceAuthentication { self.client = client } - mutating func reset() { + func reset() { credentials[0].value = nil credentials[1].value = nil isAuthorized = false diff --git a/Loop/View Controllers/BolusViewController.swift b/Loop/View Controllers/BolusViewController.swift index 53b9045857..2fb9c5b208 100644 --- a/Loop/View Controllers/BolusViewController.swift +++ b/Loop/View Controllers/BolusViewController.swift @@ -39,7 +39,7 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex bolusAmountTextField.becomeFirstResponder() - AnalyticsManager.sharedManager.didDisplayBolusScreen() + AnalyticsManager.shared.didDisplayBolusScreen() } func generateActiveInsulinDescription(activeInsulin: Double?, pendingInsulin: Double?) -> String diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift index b19bb60d82..93d6d4a849 100644 --- a/Loop/View Controllers/SettingsTableViewController.swift +++ b/Loop/View Controllers/SettingsTableViewController.swift @@ -14,6 +14,7 @@ import MinimedKit private let ConfigCellIdentifier = "ConfigTableViewCell" +private let EnabledString = NSLocalizedString("Enabled", comment: "The detail text describing an enabled setting") private let TapToSetString = NSLocalizedString("Tap to set", comment: "The empty-state text for a configuration value") @@ -53,7 +54,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } } - AnalyticsManager.sharedManager.didDisplaySettingsScreen() + AnalyticsManager.shared.didDisplaySettingsScreen() } override func viewDidDisappear(_ animated: Bool) { @@ -118,6 +119,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu case share = 0 case nightscout case mLab + case loggly case amplitude } @@ -361,11 +363,16 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu configCell.textLabel?.text = mLabService.title configCell.detailTextLabel?.text = mLabService.databaseName ?? TapToSetString + case .loggly: + let logglyService = dataManager.logger.logglyService + + configCell.textLabel?.text = logglyService.title + configCell.detailTextLabel?.text = logglyService.isAuthorized ? EnabledString : TapToSetString case .amplitude: - let amplitudeService = AnalyticsManager.sharedManager.amplitudeService + let amplitudeService = AnalyticsManager.shared.amplitudeService configCell.textLabel?.text = amplitudeService.title - configCell.detailTextLabel?.text = amplitudeService.isAuthorized ? NSLocalizedString("Enabled", comment: "The detail text describing an enabled setting") : TapToSetString + configCell.detailTextLabel?.text = amplitudeService.isAuthorized ? EnabledString : TapToSetString } return configCell @@ -619,12 +626,22 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu self.tableView.reloadRows(at: [indexPath], with: .none) } + show(vc, sender: sender) + case .loggly: + let service = dataManager.logger.logglyService + let vc = AuthenticationViewController(authentication: service) + vc.authenticationObserver = { [unowned self] (service) in + self.dataManager.logger.logglyService = service + + self.tableView.reloadRows(at: [indexPath], with: .none) + } + show(vc, sender: sender) case .amplitude: - let service = AnalyticsManager.sharedManager.amplitudeService + let service = AnalyticsManager.shared.amplitudeService let vc = AuthenticationViewController(authentication: service) vc.authenticationObserver = { [unowned self] (service) in - AnalyticsManager.sharedManager.amplitudeService = service + AnalyticsManager.shared.amplitudeService = service self.tableView.reloadRows(at: [indexPath], with: .none) } @@ -746,22 +763,22 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu case .basalRate: if let controller = controller as? SingleValueScheduleTableViewController { dataManager.loopManager.basalRateSchedule = BasalRateSchedule(dailyItems: controller.scheduleItems, timeZone: controller.timeZone) - AnalyticsManager.sharedManager.didChangeBasalRateSchedule() + AnalyticsManager.shared.didChangeBasalRateSchedule() } case .glucoseTargetRange: if let controller = controller as? GlucoseRangeScheduleTableViewController { dataManager.loopManager.settings.glucoseTargetRangeSchedule = GlucoseRangeSchedule(unit: controller.unit, dailyItems: controller.scheduleItems, workoutRange: controller.workoutRange, timeZone: controller.timeZone) - AnalyticsManager.sharedManager.didChangeGlucoseTargetRangeSchedule() + AnalyticsManager.shared.didChangeGlucoseTargetRangeSchedule() } case let row: if let controller = controller as? DailyQuantityScheduleTableViewController { switch row { case .carbRatio: dataManager.loopManager.carbRatioSchedule = CarbRatioSchedule(unit: controller.unit, dailyItems: controller.scheduleItems, timeZone: controller.timeZone) - AnalyticsManager.sharedManager.didChangeCarbRatioSchedule() + AnalyticsManager.shared.didChangeCarbRatioSchedule() case .insulinSensitivity: dataManager.loopManager.insulinSensitivitySchedule = InsulinSensitivitySchedule(unit: controller.unit, dailyItems: controller.scheduleItems, timeZone: controller.timeZone) - AnalyticsManager.sharedManager.didChangeInsulinSensitivitySchedule() + AnalyticsManager.shared.didChangeInsulinSensitivitySchedule() default: break } diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index f66f457e0f..fac86e04aa 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -101,7 +101,7 @@ final class StatusTableViewController: ChartsTableViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - AnalyticsManager.sharedManager.didDisplayStatusScreen() + AnalyticsManager.shared.didDisplayStatusScreen() } override func viewWillDisappear(_ animated: Bool) {