Skip to content

Commit c18856f

Browse files
ericloe-cashsvc-squareup-copybara
authored andcommitted
Removes the multiple redundant logs on exceptions in favor of including
the log level and exception in the RequestLoggingInterceptor logs. If the RequestLoggingInterceptor does not log for the current request the existing ExceptionHandlingInterceptor log is preserved. Removes the log in RequestBodyLoggingInterceptor as it is always redundant. GitOrigin-RevId: 27ad4bbac3862fa4096d7fde36ff8c79abf1c749
1 parent 56b3b95 commit c18856f

File tree

6 files changed

+261
-81
lines changed

6 files changed

+261
-81
lines changed

misk-grpc-tests/src/test/kotlin/misk/grpc/MiskClientMiskServerTest.kt

+25-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package misk.grpc
22

3+
import ch.qos.logback.classic.Level
34
import com.google.inject.Guice
45
import com.google.inject.util.Modules
56
import com.squareup.wire.GrpcException
67
import com.squareup.wire.GrpcStatus
8+
import jakarta.inject.Inject
9+
import jakarta.inject.Named
10+
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
11+
import kotlin.test.assertEquals
12+
import kotlin.test.assertFailsWith
713
import kotlinx.coroutines.GlobalScope
814
import kotlinx.coroutines.channels.ReceiveChannel
915
import kotlinx.coroutines.runBlocking
@@ -16,6 +22,7 @@ import misk.logging.LogCollectorModule
1622
import misk.metrics.FakeMetrics
1723
import misk.testing.MiskTest
1824
import misk.testing.MiskTestModule
25+
import misk.web.exceptions.ExceptionHandlingInterceptor
1926
import misk.web.interceptors.RequestLoggingInterceptor
2027
import okhttp3.HttpUrl
2128
import org.assertj.core.api.Assertions.assertThat
@@ -33,11 +40,6 @@ import routeguide.Point
3340
import routeguide.RouteGuideClient
3441
import routeguide.RouteNote
3542
import wisp.logging.LogCollector
36-
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
37-
import jakarta.inject.Inject
38-
import jakarta.inject.Named
39-
import kotlin.test.assertEquals
40-
import kotlin.test.assertFailsWith
4143

4244
@MiskTest(startService = true)
4345
class MiskClientMiskServerTest {
@@ -148,6 +150,15 @@ class MiskClientMiskServerTest {
148150
// is only thrown if a properly constructed gRPC error is received.
149151
assertResponseCount(200, 0)
150152
assertResponseCount(500, 1)
153+
154+
// Confirm only RequestLoggingInterceptor logs exceptions
155+
val requestLoggingInterceptorLogs = logCollector.takeMessages(RequestLoggingInterceptor::class, Level.ERROR)
156+
assertEquals(1, requestLoggingInterceptorLogs.size)
157+
assertThat(requestLoggingInterceptorLogs[0])
158+
.contains("GetFeatureGrpcAction principal=unknown time=0.000 ns failed request=Point{latitude=-1, longitude=500}")
159+
160+
val exceptionLoggingInterceptorLogs = logCollector.takeMessages(ExceptionHandlingInterceptor::class)
161+
assertThat(exceptionLoggingInterceptorLogs).isEmpty()
151162
}
152163
}
153164

@@ -171,6 +182,15 @@ class MiskClientMiskServerTest {
171182
// is only thrown if a properly constructed gRPC error is received.
172183
assertResponseCount(200, 0)
173184
assertResponseCount(404, 1)
185+
186+
// Confirm only RequestLoggingInterceptor logs exceptions
187+
val requestLoggingInterceptorLogs = logCollector.takeMessages(RequestLoggingInterceptor::class, Level.WARN)
188+
assertEquals(1, requestLoggingInterceptorLogs.size)
189+
assertThat(requestLoggingInterceptorLogs[0])
190+
.contains("GetFeatureGrpcAction principal=unknown time=0.000 ns failed request=Point{latitude=-1, longitude=404}")
191+
192+
val exceptionLoggingInterceptorLogs = logCollector.takeMessages(ExceptionHandlingInterceptor::class)
193+
assertThat(exceptionLoggingInterceptorLogs).isEmpty()
174194
}
175195
}
176196

misk/api/misk.api

+1-1
Original file line numberDiff line numberDiff line change
@@ -1768,7 +1768,7 @@ public final class misk/web/exceptions/ActionExceptionLogLevelConfig : wisp/conf
17681768
}
17691769

17701770
public final class misk/web/exceptions/ExceptionHandlingInterceptor : misk/web/NetworkInterceptor {
1771-
public fun <init> (Ljava/lang/String;Lmisk/web/exceptions/ExceptionMapperResolver;)V
1771+
public synthetic fun <init> (Ljava/lang/String;Lmisk/web/exceptions/ExceptionMapperResolver;Lmisk/web/interceptors/hooks/RequestResponseLoggedCapture;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
17721772
public fun intercept (Lmisk/web/NetworkChain;)V
17731773
}
17741774

misk/src/main/kotlin/misk/web/exceptions/ExceptionHandlingInterceptor.kt

+48-29
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import misk.web.NetworkChain
1616
import misk.web.NetworkInterceptor
1717
import misk.web.Response
1818
import misk.web.ResponseBody
19+
import misk.web.interceptors.hooks.RequestResponseLoggedCapture
1920
import misk.web.mediatype.MediaTypes
2021
import misk.web.toResponseBody
2122
import okhttp3.Headers.Companion.toHeaders
@@ -41,9 +42,10 @@ import java.util.Base64
4142
*
4243
* TODO(isabel): Set the response body in a ThreadLocal to log in [RequestLoggingInterceptor]
4344
*/
44-
class ExceptionHandlingInterceptor(
45+
class ExceptionHandlingInterceptor private constructor(
4546
private val actionName: String,
46-
private val mapperResolver: ExceptionMapperResolver
47+
private val mapperResolver: ExceptionMapperResolver,
48+
private val requestResponseLoggedCapture: RequestResponseLoggedCapture,
4749
) : NetworkInterceptor {
4850

4951
@OptIn(ExperimentalMiskApi::class)
@@ -60,7 +62,7 @@ class ExceptionHandlingInterceptor(
6062
val response = toResponse(th, suppressLog = true, mdcTags)
6163
sendGrpcFailure(chain.httpCall, response.statusCode, toGrpcResponse(th, mdcTags))
6264
} else {
63-
val response = toResponse(th, suppressLog = false, mdcTags)
65+
val response = toResponse(th, suppressLog = requestResponseLoggedCapture.isLogged(), mdcTags)
6466
chain.httpCall.statusCode = response.statusCode
6567
sendHttpFailure(chain.httpCall, response)
6668
}
@@ -117,41 +119,40 @@ class ExceptionHandlingInterceptor(
117119
return buffer.readUtf8()
118120
}
119121

120-
private fun toResponse(th: Throwable, suppressLog: Boolean, mdcTags: Set<Tag>): Response<*> {
121-
// If the exception is a reflection wrapper, unwrap first.
122-
when (th) {
123-
is InvocationTargetException -> return toResponse(th.targetException, suppressLog, mdcTags)
124-
is UncheckedExecutionException -> return toResponse(th.cause!!, suppressLog, mdcTags)
125-
}
122+
private fun toResponse(th: Throwable, suppressLog: Boolean, mdcTags: Set<Tag>): Response<*> =
123+
unwrappedToResponse(th.unwrap(), suppressLog, mdcTags)
126124

125+
private fun unwrappedToResponse(th: Throwable, suppressLog: Boolean, mdcTags: Set<Tag>): Response<*> =
127126
// Prefer the mapper's response, if one exists.
128-
val mapper = mapperResolver.mapperFor(th)
129-
if (mapper != null) {
127+
mapperResolver.mapperFor(th)?.let {
130128
if (!suppressLog) {
131129
log.log(
132-
level = mapper.loggingLevel(th),
130+
level = it.loggingLevel(th),
133131
th = th,
134-
tags = *mdcTags.toTypedArray(),
132+
tags = mdcTags.toTypedArray(),
135133
) { "exception dispatching to $actionName" }
136134
}
137-
return mapper.toResponse(th)
138-
}
139135

136+
it.toResponse(th)
137+
}
140138
// Fall back to a default mapping.
141-
return toInternalServerError(th, mdcTags)
142-
}
139+
?: toInternalServerError(th, suppressLog, mdcTags)
140+
143141

144-
private fun toGrpcResponse(th: Throwable, mdcTags: Set<Tag>): GrpcErrorResponse = when (th) {
142+
private fun toGrpcResponse(th: Throwable, mdcTags: Set<Tag>): GrpcErrorResponse =
143+
unwrappedToGrpcResponse(th.unwrap(), mdcTags)
144+
145+
private fun unwrappedToGrpcResponse(th: Throwable, mdcTags: Set<Tag>): GrpcErrorResponse = when (th) {
145146
is UnauthenticatedException -> GrpcErrorResponse(GrpcStatus.UNAUTHENTICATED, th.message)
146147
is UnauthorizedException -> GrpcErrorResponse(GrpcStatus.PERMISSION_DENIED, th.message)
147-
is InvocationTargetException -> toGrpcResponse(th.targetException, mdcTags)
148-
is UncheckedExecutionException -> toGrpcResponse(th.cause!!, mdcTags)
149148
else -> mapperResolver.mapperFor(th)?.let {
150-
log.log(
151-
level = it.loggingLevel(th),
152-
th = th,
153-
tags = *mdcTags.toTypedArray(),
154-
) { "exception dispatching to $actionName" }
149+
if (!requestResponseLoggedCapture.isLogged()) {
150+
log.log(
151+
level = it.loggingLevel(th),
152+
th = th,
153+
tags = mdcTags.toTypedArray(),
154+
) { "exception dispatching to $actionName" }
155+
}
155156
val grpcResponse = it.toGrpcResponse(th)
156157
if (grpcResponse == null) {
157158
val httpResponse = toResponse(th, suppressLog = true, mdcTags)
@@ -162,15 +163,19 @@ class ExceptionHandlingInterceptor(
162163
} ?: GrpcErrorResponse.INTERNAL_SERVER_ERROR
163164
}
164165

165-
private fun toInternalServerError(th: Throwable, mdcTags: Set<Tag>): Response<*> {
166-
log.error(th, *mdcTags.toTypedArray()) { "unexpected error dispatching to $actionName" }
166+
private fun toInternalServerError(th: Throwable, suppressLog: Boolean, mdcTags: Set<Tag>): Response<*> {
167+
if (!suppressLog) {
168+
log.error(th = th, tags = mdcTags.toTypedArray()) { "unexpected error dispatching to $actionName" }
169+
}
167170
return INTERNAL_SERVER_ERROR_RESPONSE
168171
}
169172

170173
class Factory @Inject internal constructor(
171-
private val mapperResolver: ExceptionMapperResolver
174+
private val mapperResolver: ExceptionMapperResolver,
175+
private val requestResponseLoggedCapture: RequestResponseLoggedCapture,
172176
) : NetworkInterceptor.Factory {
173-
override fun create(action: Action) = ExceptionHandlingInterceptor(action.name, mapperResolver)
177+
override fun create(action: Action) =
178+
ExceptionHandlingInterceptor(action.name, mapperResolver, requestResponseLoggedCapture)
174179
}
175180

176181
private companion object {
@@ -200,6 +205,20 @@ fun toGrpcStatus(statusCode: Int): GrpcStatus {
200205
}
201206
}
202207

208+
/**
209+
* Unwrap [InvocationTargetException] and [UncheckedExecutionException] to find the root cause.
210+
*/
211+
internal fun Throwable.unwrap(): Throwable {
212+
var th = this
213+
while (true) {
214+
th = when (th) {
215+
is InvocationTargetException -> th.targetException
216+
is UncheckedExecutionException -> th.cause!!
217+
else -> return th
218+
}
219+
}
220+
}
221+
203222
/** Convert to a compatible base64-proto-encoded google.rpc.Status-compatible value. */
204223
private val GrpcErrorResponse.toEncodedStatusProto
205224
get() : String {

misk/src/main/kotlin/misk/web/interceptors/RequestBodyLoggingInterceptor.kt

+12-17
Original file line numberDiff line numberDiff line change
@@ -83,25 +83,20 @@ class RequestBodyLoggingInterceptor @Inject internal constructor(
8383
response = null,
8484
requestHeaders = redactedRequestHeaders.headers,
8585
responseHeaders = null,
86-
)
86+
),
8787
)
8888

89-
try {
90-
val result = chain.proceed(chain.args)
91-
val redactedResponseHeaders = HeadersCapture(chain.httpCall.responseHeaders)
92-
bodyCapture.set(
93-
RequestResponseBody(
94-
request = args,
95-
response = result,
96-
requestHeaders = redactedRequestHeaders.headers,
97-
responseHeaders = redactedResponseHeaders.headers,
98-
)
99-
)
100-
return result
101-
} catch (t: Throwable) {
102-
logger.info { "${action.name} principal=$principal failed" }
103-
throw t
104-
}
89+
val result = chain.proceed(chain.args)
90+
val redactedResponseHeaders = HeadersCapture(chain.httpCall.responseHeaders)
91+
bodyCapture.set(
92+
RequestResponseBody(
93+
request = args,
94+
response = result,
95+
requestHeaders = redactedRequestHeaders.headers,
96+
responseHeaders = redactedResponseHeaders.headers,
97+
),
98+
)
99+
return result
105100
}
106101
}
107102

misk/src/main/kotlin/misk/web/interceptors/hooks/RequestResponseLoggingHook.kt

+69-5
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import jakarta.inject.Inject
44
import jakarta.inject.Singleton
55
import misk.Action
66
import misk.MiskCaller
7+
import misk.annotation.ExperimentalMiskApi
78
import misk.random.ThreadLocalRandom
89
import misk.web.HttpCall
10+
import misk.web.exceptions.ExceptionMapperResolver
11+
import misk.web.exceptions.unwrap
912
import misk.web.interceptors.ActionLoggingConfig
1013
import misk.web.interceptors.LogRateLimiter
1114
import misk.web.interceptors.LogRateLimiter.LogBucketId
@@ -14,26 +17,34 @@ import misk.web.interceptors.RequestLoggingConfig
1417
import misk.web.interceptors.RequestLoggingInterceptor
1518
import misk.web.interceptors.RequestLoggingMode
1619
import misk.web.interceptors.RequestResponseBody
20+
import org.slf4j.event.Level
1721
import wisp.deployment.Deployment
22+
import wisp.logging.SmartTagsThreadLocalHandler
23+
import wisp.logging.Tag
1824
import wisp.logging.getLogger
19-
import wisp.logging.info
25+
import wisp.logging.log
2026
import java.time.Duration
2127
import kotlin.reflect.full.findAnnotation
2228

2329
/** For backwards compatibility, the interceptor logger is used since that is where the functionality used to live. */
2430
private val logger = getLogger<RequestLoggingInterceptor>()
2531

32+
@OptIn(ExperimentalMiskApi::class)
2633
internal class RequestResponseLoggingHook private constructor(
2734
private val action: Action,
2835
private val random: ThreadLocalRandom,
2936
private val logRateLimiter: LogRateLimiter,
37+
private val mapperResolver: ExceptionMapperResolver,
38+
private val requestResponseLoggedCapture: RequestResponseLoggedCapture,
3039
val config: ActionLoggingConfig,
3140
) : RequestResponseHook {
3241
@Singleton
3342
class Factory @Inject constructor(
3443
private val random: ThreadLocalRandom,
3544
private val logRateLimiter: LogRateLimiter,
3645
private val deployment: Deployment,
46+
private val mapperResolver: ExceptionMapperResolver,
47+
private val requestResponseLoggedCapture: RequestResponseLoggedCapture,
3748
private val configs: Set<RequestLoggingConfig>,
3849
) : RequestResponseHook.Factory {
3950
override fun create(action: Action): RequestResponseHook? {
@@ -58,7 +69,14 @@ internal class RequestResponseLoggingHook private constructor(
5869
"${action.name} @LogRequestResponse errorBodySampling must be in the range (0.0, 1.0]"
5970
}
6071

61-
return RequestResponseLoggingHook(action, random, logRateLimiter, config)
72+
return RequestResponseLoggingHook(
73+
action,
74+
random,
75+
logRateLimiter,
76+
mapperResolver,
77+
requestResponseLoggedCapture,
78+
config,
79+
)
6280
}
6381
}
6482

@@ -70,6 +88,8 @@ internal class RequestResponseLoggingHook private constructor(
7088
elapsedToString: String,
7189
error: Throwable?
7290
) {
91+
requestResponseLoggedCapture.reset()
92+
7393
val statusCode = httpCall.statusCode
7494
val isError = statusCode > 299 || error != null
7595
if (!isError && config.requestLoggingMode == RequestLoggingMode.ERROR_ONLY) {
@@ -86,11 +106,16 @@ internal class RequestResponseLoggingHook private constructor(
86106
val randomDouble = random.current().nextDouble()
87107
val includeBody = randomDouble < sampling
88108

89-
logger.info(
109+
val additionalTags: MutableSet<Tag> = mutableSetOf(
90110
"response_code" to statusCode,
91111
"response_time_millis" to elapsed.toMillis(),
92-
) {
93-
buildDescription(
112+
)
113+
114+
requestResponseLoggedCapture.onLogged()
115+
116+
val error = error?.unwrap()
117+
fun buildDescriptionImpl(): Any? {
118+
return buildDescription(
94119
caller = caller,
95120
httpCall = httpCall,
96121
elapsedToString = elapsedToString,
@@ -99,6 +124,24 @@ internal class RequestResponseLoggingHook private constructor(
99124
includeBody = includeBody,
100125
)
101126
}
127+
128+
if (error != null) {
129+
// Log with the exception and smart tags applied on error.
130+
val level = mapperResolver.mapperFor(error)?.loggingLevel(error) ?: Level.ERROR
131+
132+
logger.log(
133+
level = level,
134+
th = error,
135+
tags = (SmartTagsThreadLocalHandler.peekThreadLocalSmartTags() + additionalTags).toTypedArray(),
136+
message = ::buildDescriptionImpl,
137+
)
138+
} else {
139+
logger.log(
140+
level = Level.INFO,
141+
tags = additionalTags.toTypedArray(),
142+
message = ::buildDescriptionImpl,
143+
)
144+
}
102145
}
103146

104147
private fun buildDescription(
@@ -137,3 +180,24 @@ internal class RequestResponseLoggingHook private constructor(
137180
}
138181
}
139182
}
183+
184+
/**
185+
* Captures whether the RequestResponseLogger has already logged the response to avoid double logging.
186+
*/
187+
internal class RequestResponseLoggedCapture @Inject constructor() {
188+
companion object {
189+
private val capture = object : ThreadLocal<Boolean>() {
190+
override fun initialValue() = false
191+
}
192+
}
193+
194+
fun isLogged(): Boolean = capture.get()
195+
196+
fun onLogged() {
197+
capture.set(true)
198+
}
199+
200+
fun reset() {
201+
return capture.set(false)
202+
}
203+
}

0 commit comments

Comments
 (0)