-
Notifications
You must be signed in to change notification settings - Fork 1
VIDSOL-671: Android File Logger #112
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
Open
HapPiNeHsSs
wants to merge
17
commits into
develop
Choose a base branch
from
beejay/VIDSOL-671
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
fe17b30
Added Log Interceptor for Opentok Logs
HapPiNeHsSs 6525e64
Changed Log format to lines of JSON. Logging is INFO level. Updated t…
HapPiNeHsSs c8a67a8
Added Logging toggle and send logs button in Stats Section
HapPiNeHsSs 0b20bae
Added Log level option in settings
HapPiNeHsSs 06adb95
Changed Loglevels in files to smallcaps; Changed timestamp to int; ch…
HapPiNeHsSs a56e784
Updated Upload Log description to be clear that only Info and Error l…
HapPiNeHsSs 5b1a973
Update vonage-video-core/src/main/java/com/vonage/android/kotlin/Call.kt
HapPiNeHsSs 6006bef
Update app/src/test/java/com/vonage/android/screen/settings/SettingsA…
HapPiNeHsSs 5fa6ca9
Logging is now turned off at OpentokLogInterceptor level. Added stora…
HapPiNeHsSs 4690bda
Merge remote-tracking branch 'origin/beejay/VIDSOL-671' into beejay/V…
HapPiNeHsSs 09a4d79
Added Copilot Suggestions
HapPiNeHsSs 3077bcb
Added Copilot Suggestions for FileLogInterceptor
HapPiNeHsSs 4a22ee2
Added Copilot Suggestions for FileLogInterceptor
HapPiNeHsSs e8e93f2
fixed retention mismatch var
HapPiNeHsSs e840c64
Updated vonage-android-logger.api
HapPiNeHsSs f7ca324
increased Timeout for landing_screen to 30000
HapPiNeHsSs de1f5a3
Merge branch 'develop' into beejay/VIDSOL-671
HapPiNeHsSs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
163 changes: 163 additions & 0 deletions
163
app/src/main/java/com/vonage/android/data/ClientLogsRepository.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| package com.vonage.android.data | ||
|
|
||
| import android.content.Context | ||
| import android.net.Uri | ||
| import androidx.core.content.FileProvider | ||
| import com.vonage.android.data.network.APIService | ||
| import com.vonage.android.data.storage.ClientLogsSettingsStorage | ||
| import com.vonage.android.logging.OpenTokLoggingController | ||
| import com.vonage.logger.DefaultVonageLogger | ||
| import com.vonage.logger.LogLevel | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.SupervisorJob | ||
| import kotlinx.coroutines.launch | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.asStateFlow | ||
| import kotlinx.serialization.json.Json | ||
| import kotlinx.serialization.json.jsonObject | ||
| import okhttp3.MediaType.Companion.toMediaType | ||
| import okhttp3.RequestBody.Companion.toRequestBody | ||
| import java.io.File | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| @Singleton | ||
| class ClientLogsRepository @Inject constructor( | ||
| @param:ApplicationContext private val context: Context, | ||
| private val apiService: APIService, | ||
| private val clientLogsSettingsStorage: ClientLogsSettingsStorage, | ||
| ) { | ||
|
|
||
| private val repositoryScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) | ||
|
|
||
| private val _logsEnabled = MutableStateFlow(DefaultVonageLogger.isEnabled) | ||
| val logsEnabled: StateFlow<Boolean> = _logsEnabled.asStateFlow() | ||
|
|
||
| private val _logLevel = MutableStateFlow(DefaultVonageLogger.currentMinLogLevel) | ||
| val logLevel: StateFlow<LogLevel> = _logLevel.asStateFlow() | ||
|
|
||
| init { | ||
| repositoryScope.launch { restorePersistedSettings() } | ||
| } | ||
|
|
||
| fun warmUp() { | ||
| // No-op. Exists to force repository instantiation at app startup. | ||
| } | ||
|
|
||
| fun setLogsEnabled(enabled: Boolean) { | ||
| applyRuntimeSettings(enabled = enabled, level = _logLevel.value) | ||
| repositoryScope.launch { | ||
| clientLogsSettingsStorage.saveLogsEnabled(enabled) | ||
| } | ||
| } | ||
|
|
||
| fun setLogLevel(level: LogLevel) { | ||
| applyRuntimeSettings(enabled = _logsEnabled.value, level = level) | ||
| repositoryScope.launch { | ||
| clientLogsSettingsStorage.saveLogLevel(level) | ||
| } | ||
| } | ||
|
|
||
| fun getLatestLogFile(): File? { | ||
| val logDir = File(context.filesDir, DefaultVonageLogger.LOGS_DIRECTORY_NAME) | ||
| if (!logDir.exists() || !logDir.isDirectory) return null | ||
| return logDir.listFiles { file -> | ||
| file.isFile && file.name.endsWith(LOG_FILE_SUFFIX) | ||
| }?.maxByOrNull { it.name } | ||
| } | ||
|
|
||
| fun getLatestLogUri(): Uri? { | ||
| val file = getLatestLogFile() ?: return null | ||
| return FileProvider.getUriForFile( | ||
| context, | ||
| "${context.packageName}.fileprovider", | ||
| file, | ||
| ) | ||
| } | ||
|
|
||
| suspend fun sendLogs(): SendClientLogsResult { | ||
| val payload = buildPayload() ?: return SendClientLogsResult.NoLogsAvailable | ||
| return runCatching { | ||
| apiService.sendClientLogs(payload.toRequestBody(JSON_MEDIA_TYPE)) | ||
| }.fold( | ||
| onSuccess = { response -> | ||
| if (response.isSuccessful) { | ||
| SendClientLogsResult.Success | ||
| } else { | ||
| SendClientLogsResult.Failure | ||
| } | ||
| }, | ||
| onFailure = { | ||
| SendClientLogsResult.Failure | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| private fun buildPayload(): String? { | ||
| val logDir = File(context.filesDir, DefaultVonageLogger.LOGS_DIRECTORY_NAME) | ||
| if (!logDir.exists() || !logDir.isDirectory) return null | ||
|
|
||
| val latestLogFile = logDir.listFiles { file -> | ||
| file.isFile && file.name.endsWith(LOG_FILE_SUFFIX) | ||
| } | ||
| ?.maxByOrNull { it.name } | ||
|
|
||
| val entries = latestLogFile | ||
| ?.readLines(Charsets.UTF_8) | ||
| ?.filter(String::isNotBlank) | ||
| ?.filter(::isUploadableLevel) | ||
| .orEmpty() | ||
|
|
||
| if (entries.isEmpty()) return null | ||
|
|
||
| return entries.joinToString( | ||
| separator = ",", | ||
| prefix = "[", | ||
| postfix = "]", | ||
| ) | ||
| } | ||
|
|
||
| private suspend fun restorePersistedSettings() { | ||
| val persistedEnabled = clientLogsSettingsStorage.getLogsEnabled() ?: false | ||
| val persistedLevel = clientLogsSettingsStorage.getLogLevel() ?: _logLevel.value | ||
| applyRuntimeSettings(enabled = persistedEnabled, level = persistedLevel) | ||
| } | ||
|
|
||
| private fun applyRuntimeSettings(enabled: Boolean, level: LogLevel) { | ||
| DefaultVonageLogger.setEnabled(enabled) | ||
| DefaultVonageLogger.setMinLogLevel(level) | ||
| _logsEnabled.value = enabled | ||
| _logLevel.value = level | ||
| OpenTokLoggingController.apply(enabled = enabled, minLogLevel = level) | ||
| } | ||
|
|
||
| companion object { | ||
| private const val LOG_FILE_SUFFIX = ".json.log" | ||
| private val JSON_MEDIA_TYPE = "application/json".toMediaType() | ||
| private val LOG_LEVELS_TO_UPLOAD = setOf("error", "info") | ||
|
|
||
| private fun isUploadableLevel(logLine: String): Boolean { | ||
| val level = runCatching { | ||
| Json.parseToJsonElement(logLine) | ||
| .jsonObject["level"] | ||
| ?.toString() | ||
| ?.trim('"') | ||
| ?.lowercase() | ||
| }.getOrNull() | ||
|
|
||
| return level in LOG_LEVELS_TO_UPLOAD | ||
| } | ||
| } | ||
| } | ||
|
|
||
| sealed interface SendClientLogsResult { | ||
| data object Success : SendClientLogsResult | ||
| data object NoLogsAvailable : SendClientLogsResult | ||
| data object Failure : SendClientLogsResult | ||
| } | ||
|
|
||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
app/src/main/java/com/vonage/android/data/storage/ClientLogsSettingsStorage.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package com.vonage.android.data.storage | ||
|
|
||
| import androidx.datastore.preferences.core.edit | ||
| import com.vonage.android.data.storage.GlobalDataStorage.Companion.CLIENT_LOGS_ENABLED | ||
| import com.vonage.android.data.storage.GlobalDataStorage.Companion.CLIENT_LOG_LEVEL | ||
| import com.vonage.android.util.ext.get | ||
| import com.vonage.logger.LogLevel | ||
| import javax.inject.Inject | ||
|
|
||
| class ClientLogsSettingsStorage @Inject constructor( | ||
| private val globalDataStorage: GlobalDataStorage, | ||
| ) { | ||
| suspend fun saveLogsEnabled(enabled: Boolean) { | ||
| globalDataStorage.edit { preferences -> | ||
| preferences[CLIENT_LOGS_ENABLED] = enabled | ||
| } | ||
| } | ||
|
|
||
| suspend fun getLogsEnabled(): Boolean? = globalDataStorage.get(CLIENT_LOGS_ENABLED) | ||
|
|
||
| suspend fun saveLogLevel(level: LogLevel) { | ||
| globalDataStorage.edit { preferences -> | ||
| preferences[CLIENT_LOG_LEVEL] = level.name | ||
| } | ||
| } | ||
|
|
||
| suspend fun getLogLevel(): LogLevel? = globalDataStorage | ||
| .get(CLIENT_LOG_LEVEL) | ||
| ?.let { value -> runCatching { LogLevel.valueOf(value) }.getOrNull() } | ||
| } | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
app/src/main/java/com/vonage/android/logging/OpenTokLoggingController.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package com.vonage.android.logging | ||
|
|
||
| import com.opentok.android.OpenTokConfig | ||
| import com.vonage.logger.LogLevel | ||
| import com.vonage.logger.VonageLogger | ||
| import com.vonage.logger.interceptor.OpenTokLogcatInterceptor | ||
| import com.vonage.logger.vonageLogger | ||
|
|
||
| /** | ||
| * Keeps OpenTok SDK log emission and interception aligned with app logging settings. | ||
| */ | ||
| object OpenTokLoggingController { | ||
|
|
||
| private val lock = Any() | ||
|
|
||
| @Volatile | ||
| private var interceptor: OpenTokLogcatInterceptor? = null | ||
|
|
||
| @Volatile | ||
| private var currentMinLogLevel: LogLevel? = null | ||
|
|
||
| fun apply( | ||
| enabled: Boolean, | ||
| minLogLevel: LogLevel, | ||
| logger: VonageLogger = vonageLogger, | ||
| ) { | ||
| synchronized(lock) { | ||
| if (!enabled) { | ||
| setOpenTokLogsEnabled(false) | ||
| interceptor?.stop() | ||
| interceptor = null | ||
| currentMinLogLevel = null | ||
| return | ||
| } | ||
|
|
||
| setOpenTokLogsEnabled(true) | ||
|
|
||
| if (interceptor != null && currentMinLogLevel == minLogLevel) return | ||
|
|
||
| interceptor?.stop() | ||
| interceptor = OpenTokLogcatInterceptor( | ||
| logger = logger, | ||
| minLogLevel = minLogLevel, | ||
| ).also { it.start() } | ||
| currentMinLogLevel = minLogLevel | ||
| } | ||
| } | ||
|
|
||
| private fun setOpenTokLogsEnabled(enabled: Boolean) { | ||
| OpenTokConfig.setWebRTCLogs(enabled) | ||
| OpenTokConfig.setOTKitLogs(enabled) | ||
| OpenTokConfig.setJNILogs(enabled) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Extract file access behind a
LogFileProviderinterfaceThe repository currently depends on Android's
Contextdirectly, which ties it to the Android framework and makes unit testing harder (requires Robolectric or instrumented tests).I'd suggest introducing a
LogFileProviderinterface that encapsulates all file and URI resolution logic, with anAndroidLogFileProviderimplementation that holds theContext. The repository would depend only on the interface.Why:
LogFileProviderthat returns files from a temp directory, no Android dependencies needed.FileProviderURIs lives in one place. If the storage strategy changes (e.g. different directory, different naming convention), only the provider needs updating.The interface would expose methods like
getLatestLogFile()andgetLatestLogUri(), and the Android implementation would hold theContextinternally. The repository wouldn't need to know anything aboutfilesDir,FileProvider, orpackageName.