Skip to content

Commit 17873b0

Browse files
gpuntoVelikovPetar
andauthored
Allow clients to configure logging (#69)
* Allow passing a custom logger * Rename CustomLoggerProvider to FeedsLoggerProvider * Remove okhttp level from public enum and add docs --------- Co-authored-by: Petar Velikov <[email protected]>
1 parent eb78aea commit 17873b0

File tree

9 files changed

+266
-9
lines changed

9 files changed

+266
-9
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-feeds-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.getstream.feeds.android.client.api.logging
17+
18+
/**
19+
* Represents the logging levels for HTTP requests and responses.
20+
* - None: No logging.
21+
* - Basic: Logs request and response lines.
22+
* - Headers: Logs request and response lines along with their respective headers.
23+
* - Body: Logs request and response lines, headers, and bodies (if present).
24+
*/
25+
public enum class HttpLoggingLevel {
26+
/** No logging. */
27+
None,
28+
29+
/** Logs request and response lines. */
30+
Basic,
31+
32+
/** Logs request and response lines along with their respective headers. */
33+
Headers,
34+
35+
/** Logs request and response lines, headers, and bodies (if present). */
36+
Body,
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-feeds-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.getstream.feeds.android.client.api.logging
17+
18+
/** A logger interface for logging messages at different levels. */
19+
public interface Logger {
20+
/**
21+
* Logs a message at the specified level with an optional throwable.
22+
*
23+
* @param level The log level.
24+
* @param tag A tag to identify the source of the log message.
25+
* @param throwable An optional throwable associated with the log message.
26+
* @param message A lambda that produces the log message.
27+
*/
28+
public fun log(level: Level, tag: String, throwable: Throwable? = null, message: () -> String)
29+
30+
public enum class Level {
31+
Verbose,
32+
Debug,
33+
Info,
34+
Warning,
35+
Error,
36+
}
37+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-feeds-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.getstream.feeds.android.client.api.logging
17+
18+
/**
19+
* Configuration for logging within the FeedsClient.
20+
*
21+
* @property customLogger An optional custom logger implementation. If not provided, a default
22+
* logger will be used.
23+
* @property httpLoggingLevel What level of HTTP logging to use. Defaults to
24+
* [HttpLoggingLevel.Basic].
25+
*/
26+
public class LoggingConfig(
27+
public val customLogger: Logger? = null,
28+
public val httpLoggingLevel: HttpLoggingLevel = HttpLoggingLevel.Basic,
29+
)

stream-feeds-android-client/src/main/kotlin/io/getstream/feeds/android/client/api/model/FeedsConfig.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,18 @@
1616
package io.getstream.feeds.android.client.api.model
1717

1818
import io.getstream.feeds.android.client.api.file.FeedUploader
19+
import io.getstream.feeds.android.client.api.logging.LoggingConfig
1920

2021
/**
2122
* Configuration class for the Stream Feeds Android client. This class contains all the
2223
* configuration options needed to customize the behavior of the Stream Feeds client, such as the
2324
* option for customizing the CDN.
2425
*
2526
* @param customUploader Optional [FeedUploader] implementation for overriding the default CDN.
27+
* @param loggingConfig Configuration for logging within the FeedsClient. See [LoggingConfig] for
28+
* more details.
2629
*/
27-
public class FeedsConfig(public val customUploader: FeedUploader? = null)
30+
public class FeedsConfig(
31+
public val customUploader: FeedUploader? = null,
32+
public val loggingConfig: LoggingConfig = LoggingConfig(),
33+
)

stream-feeds-android-client/src/main/kotlin/io/getstream/feeds/android/client/internal/client/Create.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import androidx.lifecycle.ProcessLifecycleOwner
2222
import io.getstream.android.core.api.StreamClient
2323
import io.getstream.android.core.api.authentication.StreamTokenManager
2424
import io.getstream.android.core.api.authentication.StreamTokenProvider
25-
import io.getstream.android.core.api.log.StreamLogger
2625
import io.getstream.android.core.api.log.StreamLoggerProvider
2726
import io.getstream.android.core.api.model.config.StreamClientSerializationConfig
2827
import io.getstream.android.core.api.model.config.StreamHttpConfig
@@ -53,6 +52,8 @@ import io.getstream.feeds.android.client.internal.client.reconnect.FeedWatchHand
5352
import io.getstream.feeds.android.client.internal.client.reconnect.lifecycle.StreamLifecycleObserver
5453
import io.getstream.feeds.android.client.internal.file.StreamFeedUploader
5554
import io.getstream.feeds.android.client.internal.http.FeedsSingleFlightApi
55+
import io.getstream.feeds.android.client.internal.logging.createLoggerProvider
56+
import io.getstream.feeds.android.client.internal.logging.createLoggingInterceptor
5657
import io.getstream.feeds.android.client.internal.repository.ActivitiesRepositoryImpl
5758
import io.getstream.feeds.android.client.internal.repository.AppRepositoryImpl
5859
import io.getstream.feeds.android.client.internal.repository.BookmarksRepositoryImpl
@@ -73,7 +74,6 @@ import kotlinx.coroutines.SupervisorJob
7374
import kotlinx.coroutines.flow.MutableSharedFlow
7475
import kotlinx.coroutines.plus
7576
import okhttp3.OkHttpClient
76-
import okhttp3.logging.HttpLoggingInterceptor
7777
import retrofit2.Retrofit
7878
import retrofit2.converter.moshi.MoshiConverterFactory
7979
import retrofit2.converter.scalars.ScalarsConverterFactory
@@ -159,11 +159,7 @@ internal fun createFeedsClient(
159159
config: FeedsConfig,
160160
): FeedsClient {
161161

162-
val logProvider =
163-
StreamLoggerProvider.Companion.defaultAndroidLogger(
164-
minLevel = StreamLogger.LogLevel.Verbose,
165-
honorAndroidIsLoggable = false,
166-
)
162+
val logProvider = createLoggerProvider(config.loggingConfig.customLogger)
167163
val logger = logProvider.taggedLogger("FeedsClient")
168164

169165
// Setup coroutine scope for the client
@@ -195,7 +191,9 @@ internal fun createFeedsClient(
195191
// HTTP Configuration
196192
val okHttpBuilder =
197193
OkHttpClient.Builder()
198-
.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
194+
.addInterceptor(
195+
createLoggingInterceptor(logProvider, config.loggingConfig.httpLoggingLevel)
196+
)
199197

200198
val client =
201199
createStreamCoreClient(
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-feeds-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.getstream.feeds.android.client.internal.logging
17+
18+
import io.getstream.android.core.api.log.StreamLogger
19+
import io.getstream.android.core.api.log.StreamLogger.LogLevel as CoreLogLevel
20+
import io.getstream.android.core.api.log.StreamLoggerProvider
21+
import io.getstream.feeds.android.client.api.logging.Logger
22+
23+
internal class FeedsLoggerProvider(private val customLogger: Logger) : StreamLoggerProvider {
24+
override fun taggedLogger(tag: String): StreamLogger = FeedsTaggedLogger(customLogger, tag)
25+
}
26+
27+
private class FeedsTaggedLogger(private val customLogger: Logger, private val tag: String) :
28+
StreamLogger {
29+
override fun log(level: CoreLogLevel, throwable: Throwable?, message: () -> String) =
30+
customLogger.log(level.map(), tag, throwable, message)
31+
}
32+
33+
private fun CoreLogLevel.map(): Logger.Level =
34+
when (this) {
35+
CoreLogLevel.Verbose -> Logger.Level.Verbose
36+
CoreLogLevel.Debug -> Logger.Level.Debug
37+
CoreLogLevel.Info -> Logger.Level.Info
38+
CoreLogLevel.Warning -> Logger.Level.Warning
39+
CoreLogLevel.Error -> Logger.Level.Error
40+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-feeds-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.getstream.feeds.android.client.internal.logging
17+
18+
import io.getstream.android.core.api.log.StreamLoggerProvider
19+
import io.getstream.feeds.android.client.api.logging.HttpLoggingLevel
20+
import io.getstream.feeds.android.client.api.logging.Logger
21+
import okhttp3.Interceptor
22+
import okhttp3.logging.HttpLoggingInterceptor
23+
24+
internal fun createLoggerProvider(customLogger: Logger?): StreamLoggerProvider =
25+
customLogger?.let(::FeedsLoggerProvider) ?: StreamLoggerProvider.defaultAndroidLogger()
26+
27+
internal fun createLoggingInterceptor(
28+
provider: StreamLoggerProvider,
29+
level: HttpLoggingLevel,
30+
): Interceptor {
31+
val logger = provider.taggedLogger("FeedHTTP")
32+
33+
return HttpLoggingInterceptor(logger = { logger.i { it } }).setLevel(level.toOkHttpLevel())
34+
}
35+
36+
private fun HttpLoggingLevel.toOkHttpLevel(): HttpLoggingInterceptor.Level =
37+
when (this) {
38+
HttpLoggingLevel.None -> HttpLoggingInterceptor.Level.NONE
39+
HttpLoggingLevel.Basic -> HttpLoggingInterceptor.Level.BASIC
40+
HttpLoggingLevel.Headers -> HttpLoggingInterceptor.Level.HEADERS
41+
HttpLoggingLevel.Body -> HttpLoggingInterceptor.Level.BODY
42+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-feeds-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.getstream.feeds.android.client.internal.logging
17+
18+
import io.getstream.android.core.api.log.StreamLogger
19+
import io.getstream.feeds.android.client.api.logging.Logger
20+
import io.mockk.mockk
21+
import io.mockk.verify
22+
import org.junit.Test
23+
import org.junit.runner.RunWith
24+
import org.junit.runners.Parameterized
25+
26+
@RunWith(Parameterized::class)
27+
internal class CustomLoggerProviderTest(
28+
private val logLevel: StreamLogger.LogLevel,
29+
private val expectedLevel: Logger.Level,
30+
) {
31+
@Test
32+
fun `on taggedLogger, return a logger that forwards the tag`() {
33+
val customLogger = mockk<Logger>(relaxed = true)
34+
val taggedLogger = FeedsLoggerProvider(customLogger).taggedLogger("a tag")
35+
val exception = Exception("an exception")
36+
37+
taggedLogger.log(logLevel, exception) { "a message" }
38+
39+
verify {
40+
customLogger.log(
41+
level = expectedLevel,
42+
tag = "a tag",
43+
throwable = exception,
44+
message = match { it() == "a message" },
45+
)
46+
}
47+
}
48+
49+
companion object {
50+
@JvmStatic
51+
@Parameterized.Parameters
52+
fun data(): Collection<Array<Any>> =
53+
listOf(
54+
arrayOf(StreamLogger.LogLevel.Verbose, Logger.Level.Verbose),
55+
arrayOf(StreamLogger.LogLevel.Debug, Logger.Level.Debug),
56+
arrayOf(StreamLogger.LogLevel.Info, Logger.Level.Info),
57+
arrayOf(StreamLogger.LogLevel.Warning, Logger.Level.Warning),
58+
arrayOf(StreamLogger.LogLevel.Error, Logger.Level.Error),
59+
)
60+
}
61+
}

stream-feeds-android-sample/src/main/java/io/getstream/feeds/android/sample/login/LoginManager.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import io.getstream.android.core.api.model.value.StreamApiKey
2626
import io.getstream.android.core.api.model.value.StreamToken
2727
import io.getstream.android.core.api.model.value.StreamUserId
2828
import io.getstream.feeds.android.client.api.FeedsClient
29+
import io.getstream.feeds.android.client.api.logging.HttpLoggingLevel
30+
import io.getstream.feeds.android.client.api.logging.LoggingConfig
31+
import io.getstream.feeds.android.client.api.model.FeedsConfig
2932
import io.getstream.feeds.android.client.api.model.User
3033
import io.getstream.feeds.android.sample.DemoAppConfig
3134
import javax.inject.Inject
@@ -93,6 +96,10 @@ constructor(
9396
return credentials.userToken
9497
}
9598
},
99+
config =
100+
FeedsConfig(
101+
loggingConfig = LoggingConfig(httpLoggingLevel = HttpLoggingLevel.Body)
102+
),
96103
)
97104

98105
return client.connect().map { UserState(user = credentials.user, client = client) }

0 commit comments

Comments
 (0)