Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LOOP-1169 - Upload device logs #100

Merged
merged 3 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions TidepoolService.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
C12E4BBB288F2215009C98A2 /* TidepoolServiceKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAACFF22E7987800E76C9F /* TidepoolServiceKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C12E4BBE288F2215009C98A2 /* TidepoolServiceKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD1B22E7988900E76C9F /* TidepoolServiceKitUI.framework */; platformFilter = ios; };
C12E4BBF288F2215009C98A2 /* TidepoolServiceKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD1B22E7988900E76C9F /* TidepoolServiceKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C1A685432C067E410071C171 /* DeviceLogUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A685422C067E410071C171 /* DeviceLogUploader.swift */; };
C1C9414629F0CB21008D3E05 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C9414529F0CB21008D3E05 /* UIImage.swift */; };
C1D0B62929848A460098D215 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62829848A460098D215 /* SettingsView.swift */; };
C1D0B62C29848BEB0098D215 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D0B62B29848BEB0098D215 /* Image.swift */; };
Expand Down Expand Up @@ -231,6 +232,7 @@
C199E4DA29C64072003D32F7 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
C1A3529629C640A5002322A5 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = "<group>"; };
C1A3529729C640A5002322A5 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = "<group>"; };
C1A685422C067E410071C171 /* DeviceLogUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLogUploader.swift; sourceTree = "<group>"; };
C1B0CFE129C786BF0045B04D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
C1B267AA2995824000BCB7C1 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
C1C9414529F0CB21008D3E05 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -402,6 +404,7 @@
A9DAAD3522E7CAC100E76C9F /* TidepoolService.swift */,
A913B37B24200C86000805C4 /* Extensions */,
A9DAAD4122E7DF9B00E76C9F /* Localizable.strings */,
C1A685422C067E410071C171 /* DeviceLogUploader.swift */,
);
path = TidepoolServiceKit;
sourceTree = "<group>";
Expand Down Expand Up @@ -752,6 +755,7 @@
A9F9F317271A046E00D19374 /* StoredCarbEntry.swift in Sources */,
A9D1AC9D27B1E3C6008C5A12 /* DoseEntry.swift in Sources */,
A9752A9B270B941C00E50750 /* SingleQuantitySchedule.swift in Sources */,
C1A685432C067E410071C171 /* DeviceLogUploader.swift in Sources */,
A9752A93270B766A00E50750 /* StoredDosingDecision.swift in Sources */,
A9752A97270B91E000E50750 /* Double.swift in Sources */,
C110888F2A39149100BA4898 /* BuildDetails.swift in Sources */,
Expand Down
130 changes: 130 additions & 0 deletions TidepoolServiceKit/DeviceLogUploader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//
// DeviceLogUploader.swift
// TidepoolServiceKit
//
// Created by Pete Schwamb on 5/28/24.
// Copyright © 2024 LoopKit Authors. All rights reserved.
//

import Foundation
import os.log
import LoopKit
import TidepoolKit

/// Periodically uploads device logs in hourly chunks to backend
actor DeviceLogUploader {
private let log = OSLog(category: "DeviceLogUploader")

private let api: TAPI

private var delegate: RemoteDataServiceDelegate?

private var logChunkDuration = TimeInterval(hours: 1)

func setDelegate(_ delegate: RemoteDataServiceDelegate?) {
self.delegate = delegate
}

init(api: TAPI) {
self.api = api

Task {
await main()
}
}

func main() async {
let backfillLimitInterval = TimeInterval(days: 2)
// Default start uploading logs from 2 days ago
var nextUploadStart = Date().addingTimeInterval(-backfillLimitInterval).dateFlooredToTimeInterval(logChunkDuration)

// Fetch device log metadata records
while true {
do {
// TODO: fetching logs is not implemented on the backend yet: awaiting https://tidepool.atlassian.net/browse/BACK-3011
// For now, we expect this to error, so the catch has been modified to break out of the loop. Once this is implemented,
// We will want to retry on error, so the break should eventually be removed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this mean that the upload will potentially upload redundant data each time Tidepool Loop is restarted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I noted this in the ticket.


var uploadMetadata = try await api.listDeviceLogs(start: Date().addingTimeInterval(-backfillLimitInterval), end: Date())
uploadMetadata.sort { a, b in
return a.endAtTime > b.endAtTime
}
if let lastEnd = uploadMetadata.last?.endAtTime {
nextUploadStart = lastEnd.dateFlooredToTimeInterval(logChunkDuration)
}
break
} catch {
log.error("Unable to fetch device log metadata: %@", String(describing: error))
try? await Task.sleep(nanoseconds: TimeInterval(minutes: 1).nanoseconds)
break // TODO: Remove when backend has implemented device log metadata fetching (see above)
}
}
// Start upload loop
while true {
let nextUploadEnd = nextUploadStart.addingTimeInterval(logChunkDuration)
let timeUntilNextUpload = nextUploadEnd.timeIntervalSinceNow
if timeUntilNextUpload > 0 {
log.debug("Waiting %@s until next upload", String(timeUntilNextUpload))
try? await Task.sleep(nanoseconds: timeUntilNextUpload.nanoseconds)
}
await upload(from: nextUploadStart, to: nextUploadEnd)
nextUploadStart = nextUploadEnd
}
}

func upload(from start: Date, to end: Date) async {
log.default("Uploading from %@ to %@", String(describing: start), String(describing: end))
do {
if let logs = try await delegate?.fetchDeviceLogs(startDate: start, endDate: end) {
log.default("Fetched %d logs", logs.count)
if logs.count > 0 {
let data = logs.map({
entry in
TDeviceLogEntry(
type: entry.type.tidepoolType,
managerIdentifier: entry.managerIdentifier,
deviceIdentifier: entry.deviceIdentifier ?? "unknown",
timestamp: entry.timestamp,
message: entry.message
)
})
do {
let metatdata = try await api.uploadDeviceLogs(logs: data, start: start, end: end)
log.default("metadata: %@", String(describing: metatdata))
print("hi")
} catch {
log.error("error uploading device logs:: %@", String(describing: error))
print("hi")
}
}
}
} catch {
log.error("Upload failed: %@", String(describing: error))
}
}
}

extension TimeInterval {
var nanoseconds: UInt64 {
return UInt64(self * 1e+9)
}
}

extension DeviceLogEntryType {
var tidepoolType: TDeviceLogEntry.TDeviceLogEntryType {
switch self {
case .send:
return .send
case .receive:
return .receive
case .error:
return .error
case .delegate:
return .delegate
case .delegateResponse:
return .delegateResponse
case .connection:
return .connection
}
}
}
Loading