feat(ios): enhance recovery logging and metrics recording
Implement comprehensive logging and observability for recovery operations: - Add HistoryDAO for Core Data history recording - recordRecovery() method with execution time tracking - recordRecoveryFailure() method with detailed error info - Query helpers for history retrieval - Enhance DailyNotificationReactivationManager logging: - Add execution time tracking (startTime/endTime) - Enhanced error logging with NSError details (domain, code, userInfo) - Comprehensive logging at each recovery step - Missed/future notification count logging - Implement Core Data persistence for recovery metrics: - Recovery execution time - Missed notification count - Rescheduled notification count - Verified notification count - Error count - Diagnostic JSON with full recovery context - Update recovery methods to record history: - Cold start recovery - Termination recovery - Boot recovery - All with timing and metrics Completes section 7.1 (Recovery Logging) and 7.2 (Metrics Recording) of iOS implementation checklist.
This commit is contained in:
300
ios/Plugin/HistoryDAO.swift
Normal file
300
ios/Plugin/HistoryDAO.swift
Normal file
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* HistoryDAO.swift
|
||||
*
|
||||
* Data Access Object (DAO) for History Core Data entity
|
||||
* Provides helper methods for recording recovery and operation history
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
* @created 2025-12-08
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
/**
|
||||
* Extension providing DAO methods for History entity
|
||||
*
|
||||
* This extension adds helper methods for recording operation history
|
||||
* including recovery operations, errors, and metrics.
|
||||
*/
|
||||
extension History {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private static let TAG = "DNP-HISTORY-DAO"
|
||||
|
||||
// MARK: - Create/Insert Methods
|
||||
|
||||
/**
|
||||
* Create a new History entity in the given context
|
||||
*
|
||||
* @param context Core Data managed object context
|
||||
* @param id Unique history identifier (UUID recommended)
|
||||
* @param refId Reference ID (e.g., notification ID, schedule ID)
|
||||
* @param kind History kind (e.g., "recovery", "fetch", "notify")
|
||||
* @param occurredAt When the event occurred
|
||||
* @param durationMs Duration in milliseconds
|
||||
* @param outcome Outcome string (e.g., "success", "failure", "skipped")
|
||||
* @param diagJson Diagnostic JSON string with additional details
|
||||
* @return Created History entity
|
||||
*/
|
||||
static func create(
|
||||
in context: NSManagedObjectContext,
|
||||
id: String,
|
||||
refId: String? = nil,
|
||||
kind: String,
|
||||
occurredAt: Date,
|
||||
durationMs: Int32 = 0,
|
||||
outcome: String,
|
||||
diagJson: String? = nil
|
||||
) -> History {
|
||||
let entity = History(context: context)
|
||||
|
||||
entity.id = id
|
||||
entity.refId = refId
|
||||
entity.kind = kind
|
||||
entity.occurredAt = occurredAt
|
||||
entity.durationMs = durationMs
|
||||
entity.outcome = outcome
|
||||
entity.diagJson = diagJson
|
||||
|
||||
print("\(Self.TAG): Created History record: kind=\(kind), outcome=\(outcome)")
|
||||
return entity
|
||||
}
|
||||
|
||||
/**
|
||||
* Create history record from dictionary
|
||||
*
|
||||
* @param context Core Data managed object context
|
||||
* @param dict Dictionary with history data
|
||||
* @return Created History entity or nil
|
||||
*/
|
||||
static func create(
|
||||
in context: NSManagedObjectContext,
|
||||
from dict: [String: Any]
|
||||
) -> History? {
|
||||
guard let id = dict["id"] as? String,
|
||||
let kind = dict["kind"] as? String,
|
||||
let outcome = dict["outcome"] as? String else {
|
||||
print("\(Self.TAG): Missing required fields")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert occurredAt from epoch milliseconds or Date
|
||||
let occurredAt: Date
|
||||
if let timeMillis = dict["occurredAt"] as? Int64 {
|
||||
occurredAt = DailyNotificationDataConversions.dateFromEpochMillis(timeMillis)
|
||||
} else if let timeDate = dict["occurredAt"] as? Date {
|
||||
occurredAt = timeDate
|
||||
} else {
|
||||
occurredAt = Date()
|
||||
}
|
||||
|
||||
let entity = History(context: context)
|
||||
entity.id = id
|
||||
entity.refId = dict["refId"] as? String
|
||||
entity.kind = kind
|
||||
entity.occurredAt = occurredAt
|
||||
entity.durationMs = DailyNotificationDataConversions.int32FromInt(
|
||||
dict["durationMs"] as? Int ?? 0
|
||||
)
|
||||
entity.outcome = outcome
|
||||
|
||||
// Convert diagJson from dictionary if needed
|
||||
if let diagDict = dict["diagJson"] as? [String: Any] {
|
||||
entity.diagJson = DailyNotificationDataConversions.jsonStringFromDictionary(diagDict)
|
||||
} else if let diagString = dict["diagJson"] as? String {
|
||||
entity.diagJson = diagString
|
||||
}
|
||||
|
||||
return entity
|
||||
}
|
||||
|
||||
/**
|
||||
* Record recovery history with metrics
|
||||
*
|
||||
* @param context Core Data managed object context
|
||||
* @param scenario Recovery scenario
|
||||
* @param missedCount Number of missed notifications
|
||||
* @param rescheduledCount Number of rescheduled notifications
|
||||
* @param verifiedCount Number of verified notifications
|
||||
* @param errors Number of errors
|
||||
* @param startTime When recovery started
|
||||
* @param endTime When recovery ended
|
||||
* @return Created History entity
|
||||
*/
|
||||
static func recordRecovery(
|
||||
in context: NSManagedObjectContext,
|
||||
scenario: String,
|
||||
missedCount: Int,
|
||||
rescheduledCount: Int,
|
||||
verifiedCount: Int,
|
||||
errors: Int,
|
||||
startTime: Date,
|
||||
endTime: Date
|
||||
) -> History {
|
||||
let durationMs = Int32((endTime.timeIntervalSince(startTime) * 1000).rounded())
|
||||
|
||||
let diagJson: [String: Any] = [
|
||||
"scenario": scenario,
|
||||
"missedCount": missedCount,
|
||||
"rescheduledCount": rescheduledCount,
|
||||
"verifiedCount": verifiedCount,
|
||||
"errors": errors,
|
||||
"durationMs": durationMs
|
||||
]
|
||||
|
||||
let diagJsonString = DailyNotificationDataConversions.jsonStringFromDictionary(diagJson) ?? "{}"
|
||||
|
||||
return create(
|
||||
in: context,
|
||||
id: UUID().uuidString,
|
||||
kind: "recovery",
|
||||
occurredAt: endTime,
|
||||
durationMs: durationMs,
|
||||
outcome: errors > 0 ? "partial_success" : "success",
|
||||
diagJson: diagJsonString
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Record recovery failure
|
||||
*
|
||||
* @param context Core Data managed object context
|
||||
* @param error Error that occurred
|
||||
* @param scenario Recovery scenario (if known)
|
||||
* @return Created History entity
|
||||
*/
|
||||
static func recordRecoveryFailure(
|
||||
in context: NSManagedObjectContext,
|
||||
error: Error,
|
||||
scenario: String? = nil
|
||||
) -> History {
|
||||
var errorInfo: [String: Any] = [
|
||||
"error": error.localizedDescription,
|
||||
"errorType": String(describing: type(of: error))
|
||||
]
|
||||
|
||||
// Add scenario if provided
|
||||
if let scenario = scenario {
|
||||
errorInfo["scenario"] = scenario
|
||||
}
|
||||
|
||||
// Add error details if available
|
||||
if let nsError = error as NSError? {
|
||||
errorInfo["errorCode"] = nsError.code
|
||||
errorInfo["errorDomain"] = nsError.domain
|
||||
if let userInfo = nsError.userInfo as? [String: Any] {
|
||||
errorInfo["userInfo"] = userInfo
|
||||
}
|
||||
}
|
||||
|
||||
let diagJsonString = DailyNotificationDataConversions.jsonStringFromDictionary(errorInfo) ?? "{}"
|
||||
|
||||
return create(
|
||||
in: context,
|
||||
id: UUID().uuidString,
|
||||
kind: "recovery",
|
||||
occurredAt: Date(),
|
||||
outcome: "failure",
|
||||
diagJson: diagJsonString
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Read/Query Methods
|
||||
|
||||
/**
|
||||
* Fetch History by ID
|
||||
*
|
||||
* @param context Core Data managed object context
|
||||
* @param id History ID
|
||||
* @return History entity or nil
|
||||
*/
|
||||
static func fetch(
|
||||
by id: String,
|
||||
in context: NSManagedObjectContext
|
||||
) -> History? {
|
||||
let request: NSFetchRequest<History> = History.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "id == %@", id)
|
||||
request.fetchLimit = 1
|
||||
|
||||
do {
|
||||
let results = try context.fetch(request)
|
||||
return results.first
|
||||
} catch {
|
||||
print("\(Self.TAG): Error fetching by id: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query by kind
|
||||
*
|
||||
* @param context Core Data managed object context
|
||||
* @param kind History kind
|
||||
* @return Array of History entities
|
||||
*/
|
||||
static func query(
|
||||
by kind: String,
|
||||
in context: NSManagedObjectContext
|
||||
) -> [History] {
|
||||
let request: NSFetchRequest<History> = History.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "kind == %@", kind)
|
||||
request.sortDescriptors = [NSSortDescriptor(key: "occurredAt", ascending: false)]
|
||||
|
||||
do {
|
||||
return try context.fetch(request)
|
||||
} catch {
|
||||
print("\(Self.TAG): Error querying by kind: \(error.localizedDescription)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query by refId
|
||||
*
|
||||
* @param context Core Data managed object context
|
||||
* @param refId Reference ID
|
||||
* @return Array of History entities
|
||||
*/
|
||||
static func query(
|
||||
by refId: String,
|
||||
in context: NSManagedObjectContext
|
||||
) -> [History] {
|
||||
let request: NSFetchRequest<History> = History.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "refId == %@", refId)
|
||||
request.sortDescriptors = [NSSortDescriptor(key: "occurredAt", ascending: false)]
|
||||
|
||||
do {
|
||||
return try context.fetch(request)
|
||||
} catch {
|
||||
print("\(Self.TAG): Error querying by refId: \(error.localizedDescription)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query by outcome
|
||||
*
|
||||
* @param context Core Data managed object context
|
||||
* @param outcome Outcome string
|
||||
* @return Array of History entities
|
||||
*/
|
||||
static func query(
|
||||
by outcome: String,
|
||||
in context: NSManagedObjectContext
|
||||
) -> [History] {
|
||||
let request: NSFetchRequest<History> = History.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "outcome == %@", outcome)
|
||||
request.sortDescriptors = [NSSortDescriptor(key: "occurredAt", ascending: false)]
|
||||
|
||||
do {
|
||||
return try context.fetch(request)
|
||||
} catch {
|
||||
print("\(Self.TAG): Error querying by outcome: \(error.localizedDescription)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user