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