Logging
Setting the Log Level
SDK Logging is started automatically when the MasterController is initialized. The default log level is KSMLogInfo
or INFO
. To get a more detailed log during debugging, it can sometimes be helpful to switch it to KSMLogTrace
or TRACE
, but this is way to verbose to use it by default. Also note that the choice of log level can have an influence on timing, so if you are e.g. investigating a bug related to a race condition, it is possible that the problem will not occur when switching to a different log level.
⚠️ IMPORTANT: Do NOT use TRACE log level in production/release applications! Otherwise you risk confidential data being written to disk. Even though the logs are encrypted we strongly advice against using TRACE level outside of testing/debugging!
We suggest to not use the log levels that are less verbose than INFO
as those do give only information on errors when they actually happen, but in practice it turns out that they almost never provide sufficient context to diagnose the reason for an error.
iOS/Swift
Under iOS, choosing a different log level may look as follows:
var logSink: KSMLogSink?
func setLogLevel(logLevel: KSMLogLevel) {
if logSink == nil {
logSink = KSMasterControllerFactory.getLogSink()
}
logSink?.setSeverityLevel(logLevel)
}
Android/Kotlin
For Android, we suggest setting the log level in the onCreate of your Application Class, as shown in this code snippet:
import com.kobil.wrapper.logger.Log
...
...
override fun onCreate() {
super.onCreate()
Log.setSeverity(Severity.TRACE)
}
Exporting Logs
The logs are written to the app's DocumentsDirectory on iOS, while on Android the app's cache dir (for example /data/data/com.kobil.mcwmpgettingstarted/cache/logs_mPower/) is used. Those directories are not accessible in release builds (at least not easily) so you will want to export the logs to an accessible dir.
iOS/Swift
For iOS, we suggest to either enable sharing the data with your PC via the Info.plist with an entry such as
<key>UIFileSharingEnabled</key>
<true/>
or to share the files via Email or the Filesharing mechanism. Anyway, in both these cases we recommend to save the files as zip before sharing. This can be done by the following snippets:
extension BaseViewController: MFMailComposeViewControllerDelegate {
func sendMail() {
/*
Get logs file
*/
guard let zippedLogsLocation = zipLogs() else {
return
}
/*
convert the zipLogs folder location into url
*/
let url = URL(fileURLWithPath: zippedLogsLocation)
/*
Check mail services enable on your device or not
*/
if !MFMailComposeViewController.canSendMail() {
print("Mail services are not available")
self.showAlert(title: "Mail Service", message: "Mail services are not available")
return
}
/*
Create instance for mailComposer
*/
let composeVC = MFMailComposeViewController()
/*
confirm MailComposer delegate
*/
composeVC.mailComposeDelegate = self
/*
Configure the fields of the interface.
*/
composeVC.setToRecipients([self.globalConst.appConfiguration?.logFileReceiverMailAddress?.iOS?.first ?? ""])
composeVC.setSubject("Ast SDK logs file!")
composeVC.setMessageBody("Please look into the attached log files.", isHTML: false)
/*
convert the logs file content into data
*/
guard let data = NSData(contentsOf: url) else {
return
}
/*
Add logs data as an attachment file.
*/
composeVC.addAttachmentData(data as Data, mimeType: "application/zip", fileName: "exportLogs.zip")
/*
Present the view controller modally.
*/
self.present(composeVC, animated: true, completion: nil)
}
func mailComposeController(_ didFinishWithcontroller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
/*
Dismiss mail composer
*/
self.dismiss(animated: true, completion: nil)
}
}
extension BaseViewController {
func exportLogs() {
guard let zippedLogsLocation = zipLogs() else {
return
}
let url = URL(fileURLWithPath: zippedLogsLocation)
let documentInteractor = UIDocumentInteractionController()
documentInteractor.url = url
documentInteractor.delegate = self
DispatchQueue.main.async {
documentInteractor.presentPreview(animated: true)
}
}
}
extension BaseViewController {
func zipLogs() -> String? {
var logFiles: [String] = []
guard let logFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.relativePath else {
return nil
}
let fileMgr = FileManager.default
fileMgr.changeCurrentDirectoryPath(logFolder)
let enumerator = fileMgr.enumerator(atPath: logFolder)
while ((enumerator?.nextObject() as? String) != nil) {
if let entry = enumerator?.nextObject() as? String {
if fileMgr.fileExists(atPath: entry){
if entry.contains("logs_mPower"){
logFiles.append(entry)
}
}
}
}
guard let documentFolder = documentsFolder()else {
return nil
}
let zippedFile = URL(fileURLWithPath: documentFolder).appendingPathComponent("exportedLogs_MCWMP.zip").path
SSZipArchive.createZipFile(atPath: zippedFile, withFilesAtPaths: logFiles)
return zippedFile
}
func documentsFolder() -> String? {
let documentsFolderPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.relativePath
return documentsFolderPath
}
}
Android/Kotlin
On Android the SDK will try to export the logs to your app's externalFilesDir, from there they can be easily retrieved through any file explorer (for example the Device Explorer in Android Studio). If externalFilesDir is not writable for some reason the SDK will try to write the logs to your app's filesDir and finally to the cacheDir. The logs will always be in a folder called logs_mPower. If you want to add some logic in your app to copy/export the logs or share them via e-mail be sure to check the following paths for a folder called logs_mPower in the giving order: getExternalFilesDir() > getFilesDir() > getCacheDir().
For sending the logs via mail we recommend zipping them first because they might consist of multiple files.
Below is an example how one could copy the SDK logs from cache dir to external storage.
/***
* This function will copy SDK logs files under [/data/data/com.companyname.appname/cache/logs_mPower/]
* into [/sdcard/Android/data/com.companyname.appname/files/...]
* If the target folder exists, contents will be deleted before copy process
*/
@JvmStatic
suspend fun exportLogsToExternalStorage(
context: Context,
onLogsReady: (filesCopied: Boolean, destinationFile: File?) -> Unit
) = withContext(Dispatchers.IO) {
val sourceLocation =
File(context.cacheDir.path + File.separator + "logs_mPower" + File.separator)
val destinationFolderName = AppConstants.APP_NAME
val targetLocation =
File(
getExternalFilesDirs(
context.applicationContext,
null
)[0].path + File.separator + destinationFolderName
)
if (targetLocation.exists()) {
targetLocation.listFiles()?.forEach { if (it.exists()) it.delete() }
}
try {
if (sourceLocation.exists()) {
val result = sourceLocation.copyRecursively(
target = targetLocation,
overwrite = true,
onError = { file, exception ->
Log.e(TAG, "exportLogsToExternalStorage -> File = [${file}]\n-> Exception = [${exception.message}]")
SKIP
})
onLogsReady.invoke(result, targetLocation)
} else {
Log.e(TAG, "exportLogsToExternalStorage -> Copy file failed. sourceLocation [${sourceLocation.path}] doesn't exists. sourceLocation = [$sourceLocation]")
onLogsReady.invoke(false, null)
}
} catch (e: Exception) {
Log.e(TAG, "exportLogsToExternalStorage -> Copy file failed : " + e.message)
onLogsReady.invoke(false, null)
}
}
// example use of the above function
fun onExportLogsBtnClick(view: View) {
val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch {
LogsManager.exportLogsToExternalStorage(application) { filesCopied, destinationFile ->
Log.d(tag, "onExportLogsBtnClick -> Operation was ${if (filesCopied) "successful" else "unsuccessful"}",)
}
}
}