1+ package com.coder.toolbox.sdk.interceptors
2+
3+ import com.coder.toolbox.CoderToolboxContext
4+ import com.coder.toolbox.settings.HttpLoggingVerbosity
5+ import okhttp3.Headers
6+ import okhttp3.Interceptor
7+ import okhttp3.MediaType
8+ import okhttp3.RequestBody
9+ import okhttp3.Response
10+ import okhttp3.ResponseBody
11+ import okio.Buffer
12+ import java.nio.charset.StandardCharsets
13+
14+ class LoggingInterceptor (private val context : CoderToolboxContext ) : Interceptor {
15+ override fun intercept (chain : Interceptor .Chain ): Response {
16+ val logLevel = context.settingsStore.httpClientLogLevel
17+ if (logLevel == HttpLoggingVerbosity .NONE ) {
18+ return chain.proceed(chain.request())
19+ }
20+ val request = chain.request()
21+ val requestLog = StringBuilder ()
22+ requestLog.append(" request --> ${request.method} ${request.url} \n " )
23+ if (logLevel == HttpLoggingVerbosity .HEADERS ) {
24+ requestLog.append(request.headers.toSanitizedString())
25+ }
26+ if (logLevel == HttpLoggingVerbosity .BODY ) {
27+ request.body.toPrintableString()?.let {
28+ requestLog.append(it)
29+ }
30+ }
31+ context.logger.info(requestLog.toString())
32+
33+ val response = chain.proceed(request)
34+ val responseLog = StringBuilder ()
35+ responseLog.append(" response <-- ${response.code} ${response.message} ${request.url} \n " )
36+ if (logLevel == HttpLoggingVerbosity .HEADERS ) {
37+ responseLog.append(response.headers.toSanitizedString())
38+ }
39+ if (logLevel == HttpLoggingVerbosity .BODY ) {
40+ response.body.toPrintableString()?.let {
41+ responseLog.append(it)
42+ }
43+ }
44+
45+ context.logger.info(responseLog.toString())
46+ return response
47+ }
48+
49+ private fun Headers.toSanitizedString (): String {
50+ val result = StringBuilder ()
51+ this .forEach {
52+ if (it.first == " Coder-Session-Token" || it.first == " Proxy-Authorization" ) {
53+ result.append(" ${it.first} : <redacted>\n " )
54+ } else {
55+ result.append(" ${it.first} : ${it.second} \n " )
56+ }
57+ }
58+ return result.toString()
59+ }
60+
61+ /* *
62+ * Converts a RequestBody to a printable string representation.
63+ * Handles different content types appropriately.
64+ *
65+ * @return String representation of the body, or metadata if not readable
66+ */
67+ fun RequestBody?.toPrintableString (): String? {
68+ if (this == null ) {
69+ return null
70+ }
71+
72+ if (! contentType().isPrintable()) {
73+ return " [Binary request body: ${contentLength().formatBytes()} , Content-Type: ${contentType()} ]\n "
74+ }
75+
76+ return try {
77+ val buffer = Buffer ()
78+ writeTo(buffer)
79+
80+ val charset = contentType()?.charset() ? : StandardCharsets .UTF_8
81+ buffer.readString(charset)
82+ } catch (e: Exception ) {
83+ " [Error reading request body: ${e.message} ]\n "
84+ }
85+ }
86+
87+ /* *
88+ * Converts a ResponseBody to a printable string representation.
89+ * Handles different content types appropriately.
90+ *
91+ * @return String representation of the body, or metadata if not readable
92+ */
93+ fun ResponseBody?.toPrintableString (): String? {
94+ if (this == null ) {
95+ return null
96+ }
97+
98+ if (! contentType().isPrintable()) {
99+ return " [Binary response body: ${contentLength().formatBytes()} , Content-Type: ${contentType()} ]\n "
100+ }
101+
102+ return try {
103+ val source = source()
104+ source.request(Long .MAX_VALUE )
105+ val charset = contentType()?.charset() ? : StandardCharsets .UTF_8
106+ source.buffer.clone().readString(charset)
107+ } catch (e: Exception ) {
108+ " [Error reading response body: ${e.message} ]\n "
109+ }
110+ }
111+
112+ /* *
113+ * Checks if a MediaType represents printable/readable content
114+ */
115+ private fun MediaType?.isPrintable (): Boolean {
116+ if (this == null ) return false
117+
118+ return when {
119+ // Text types
120+ type == " text" -> true
121+
122+ // JSON variants
123+ subtype == " json" -> true
124+ subtype.endsWith(" +json" ) -> true
125+
126+ // Default to non-printable for safety
127+ else -> false
128+ }
129+ }
130+
131+ /* *
132+ * Formats byte count in human-readable format
133+ */
134+ private fun Long.formatBytes (): String {
135+ return when {
136+ this < 0 -> " unknown size"
137+ this < 1024 -> " ${this } B"
138+ this < 1024 * 1024 -> " ${this / 1024 } KB"
139+ this < 1024 * 1024 * 1024 -> " ${this / (1024 * 1024 )} MB"
140+ else -> " ${this / (1024 * 1024 * 1024 )} GB"
141+ }
142+ }
143+ }
0 commit comments