Skip to content

Commit 0c0fc1b

Browse files
authored
Merge pull request #165 from ashtanko/feature/http_logger
Add http logger
2 parents c5be1d4 + eb40986 commit 0c0fc1b

File tree

15 files changed

+915
-12
lines changed

15 files changed

+915
-12
lines changed

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,23 @@
2222

2323
### Metrics
2424
```text
25-
15258 number of properties
26-
10538 number of functions
27-
8941 number of classes
28-
239 number of packages
29-
3533 number of kt files
25+
15288 number of properties
26+
10573 number of functions
27+
8954 number of classes
28+
240 number of packages
29+
3543 number of kt files
3030
```
3131

3232

3333
### Complexity Report
3434
```text
35-
266898 lines of code (loc)
36-
165936 source lines of code (sloc)
37-
121212 logical lines of code (lloc)
38-
72548 comment lines of code (cloc)
39-
25066 cyclomatic complexity (mcc)
40-
20459 cognitive complexity
35+
267458 lines of code (loc)
36+
166374 source lines of code (sloc)
37+
121548 logical lines of code (lloc)
38+
72562 comment lines of code (cloc)
39+
25100 cyclomatic complexity (mcc)
40+
20431 cognitive complexity
4141
0 number of total code smells
4242
43 comment source ratio
4343
206 mcc per 1,000 lloc
44-
```
44+
```

api/Kotlin-Lab.api

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21440,6 +21440,96 @@ public final class dev/shtanko/grasp/polymorphism/example1/Triangle : dev/shtank
2144021440
public final class dev/shtanko/grasp/polymorphism/example1/Triangle$Companion {
2144121441
}
2144221442

21443+
public abstract interface class dev/shtanko/http/ConnectionFactory {
21444+
public abstract fun invoke ()Ljava/sql/Connection;
21445+
}
21446+
21447+
public final class dev/shtanko/http/CustomInterceptor : okhttp3/Interceptor {
21448+
public static final field Companion Ldev/shtanko/http/CustomInterceptor$Companion;
21449+
public synthetic fun <init> (Ldev/shtanko/http/HttpLogger;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
21450+
public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response;
21451+
}
21452+
21453+
public final class dev/shtanko/http/CustomInterceptor$Companion {
21454+
public final fun create (Ldev/shtanko/http/HttpLogger;)Ldev/shtanko/http/CustomInterceptor;
21455+
public static synthetic fun create$default (Ldev/shtanko/http/CustomInterceptor$Companion;Ldev/shtanko/http/HttpLogger;ILjava/lang/Object;)Ldev/shtanko/http/CustomInterceptor;
21456+
}
21457+
21458+
public final class dev/shtanko/http/DbHttpLogger : dev/shtanko/http/HttpLogger {
21459+
public fun <init> (Ldev/shtanko/http/HttpLogRepository;)V
21460+
public fun logRequest (Lokhttp3/Request;)V
21461+
public fun logResponse (Lokhttp3/Response;D)V
21462+
}
21463+
21464+
public final class dev/shtanko/http/DefaultHttpLogger : dev/shtanko/http/HttpLogger {
21465+
public fun <init> ()V
21466+
public fun logRequest (Lokhttp3/Request;)V
21467+
public fun logResponse (Lokhttp3/Response;D)V
21468+
}
21469+
21470+
public final class dev/shtanko/http/FileSqliteConnectionFactory : dev/shtanko/http/ConnectionFactory {
21471+
public fun <init> ()V
21472+
public fun <init> (Ljava/lang/String;)V
21473+
public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
21474+
public fun invoke ()Ljava/sql/Connection;
21475+
}
21476+
21477+
public final class dev/shtanko/http/HttpLogEntry {
21478+
public fun <init> (Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;DLjava/time/Instant;)V
21479+
public synthetic fun <init> (Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;DLjava/time/Instant;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
21480+
public final fun component1 ()Ljava/lang/Long;
21481+
public final fun component2 ()Ljava/lang/String;
21482+
public final fun component3 ()Ljava/lang/String;
21483+
public final fun component4 ()Ljava/lang/String;
21484+
public final fun component5 ()I
21485+
public final fun component6 ()Ljava/lang/String;
21486+
public final fun component7 ()D
21487+
public final fun component8 ()Ljava/time/Instant;
21488+
public final fun copy (Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;DLjava/time/Instant;)Ldev/shtanko/http/HttpLogEntry;
21489+
public static synthetic fun copy$default (Ldev/shtanko/http/HttpLogEntry;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;DLjava/time/Instant;ILjava/lang/Object;)Ldev/shtanko/http/HttpLogEntry;
21490+
public fun equals (Ljava/lang/Object;)Z
21491+
public final fun getDurationMs ()D
21492+
public final fun getId ()Ljava/lang/Long;
21493+
public final fun getMethod ()Ljava/lang/String;
21494+
public final fun getRequestHeaders ()Ljava/lang/String;
21495+
public final fun getResponseCode ()I
21496+
public final fun getResponseHeaders ()Ljava/lang/String;
21497+
public final fun getTimestamp ()Ljava/time/Instant;
21498+
public final fun getUrl ()Ljava/lang/String;
21499+
public fun hashCode ()I
21500+
public fun toString ()Ljava/lang/String;
21501+
}
21502+
21503+
public final class dev/shtanko/http/HttpLogRepository {
21504+
public static final field Companion Ldev/shtanko/http/HttpLogRepository$Companion;
21505+
public fun <init> (Ljava/sql/Connection;)V
21506+
public final fun count ()J
21507+
public final fun deleteOlderThan (Ljava/time/Instant;)I
21508+
public final fun getAll ()Ljava/util/List;
21509+
public final fun getById (J)Ldev/shtanko/http/HttpLogEntry;
21510+
public final fun getByUrlPattern (Ljava/lang/String;)Ljava/util/List;
21511+
public final fun init ()V
21512+
public final fun insert (Ldev/shtanko/http/HttpLogEntry;)V
21513+
}
21514+
21515+
public final class dev/shtanko/http/HttpLogRepository$Companion {
21516+
}
21517+
21518+
public abstract interface class dev/shtanko/http/HttpLogger {
21519+
public abstract fun logRequest (Lokhttp3/Request;)V
21520+
public abstract fun logResponse (Lokhttp3/Response;D)V
21521+
}
21522+
21523+
public final class dev/shtanko/http/InMemorySqliteConnectionFactory : dev/shtanko/http/ConnectionFactory {
21524+
public fun <init> ()V
21525+
public fun invoke ()Ljava/sql/Connection;
21526+
}
21527+
21528+
public final class dev/shtanko/http/Main {
21529+
public static final fun main ()V
21530+
public static synthetic fun main ([Ljava/lang/String;)V
21531+
}
21532+
2144321533
public final class dev/shtanko/jmm/memory/ManualResourceManagementKt {
2144421534
public static final fun readFromFile (Ljava/lang/String;)V
2144521535
}

build.gradle.kts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ repositories {
7373
application {
7474
mainClass.set("link.kotlin.scripts.Application")
7575
mainClass.set("dev.shtanko.report.ReportParserKt")
76+
mainClass.set("dev.shtanko.http.Main")
77+
}
78+
79+
tasks.register<JavaExec>("dev.shtanko.http.Main.main()") {
80+
mainClass.set("dev.shtanko.http.Main")
81+
group = "application"
82+
mainClass.set("dev.shtanko.http.Main")
83+
classpath = sourceSets["main"].runtimeClasspath
7684
}
7785

7886
pitest {
@@ -368,6 +376,7 @@ dependencies {
368376
testImplementation(turbine)
369377
testImplementation(truth)
370378
}
379+
implementation("org.xerial:sqlite-jdbc:3.45.3.0")
371380
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
372381
}
373382

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package dev.shtanko.http
2+
3+
import java.sql.Connection
4+
5+
fun interface ConnectionFactory {
6+
operator fun invoke(): Connection
7+
}
8+
9+
class FileSqliteConnectionFactory(
10+
private val filePath: String = "test.db",
11+
) : ConnectionFactory {
12+
override fun invoke(): Connection {
13+
val url = "jdbc:sqlite:$filePath"
14+
return java.sql.DriverManager.getConnection(url)
15+
}
16+
}
17+
18+
class InMemorySqliteConnectionFactory : ConnectionFactory {
19+
override fun invoke(): Connection {
20+
val url = "jdbc:sqlite::memory:"
21+
return java.sql.DriverManager.getConnection(url)
22+
}
23+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.shtanko.http
2+
3+
import okhttp3.Interceptor
4+
import okhttp3.Response
5+
6+
private const val NANOS_PER_MILLI = 1_000_000.0
7+
8+
class CustomInterceptor private constructor(
9+
private val logger: HttpLogger = DefaultHttpLogger(),
10+
) : Interceptor {
11+
override fun intercept(chain: Interceptor.Chain): Response {
12+
val request = chain.request()
13+
logger.logRequest(request)
14+
15+
val startNs = System.nanoTime()
16+
val response = chain.proceed(request)
17+
val tookMs = (System.nanoTime() - startNs) / NANOS_PER_MILLI
18+
19+
logger.logResponse(response, tookMs)
20+
return response
21+
}
22+
23+
companion object {
24+
fun create(logger: HttpLogger = DefaultHttpLogger()): CustomInterceptor =
25+
CustomInterceptor(logger)
26+
}
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.shtanko.http
2+
3+
import java.time.Instant
4+
import okhttp3.Request
5+
import okhttp3.Response
6+
7+
class DbHttpLogger(private val repository: HttpLogRepository) : HttpLogger {
8+
override fun logRequest(request: Request) {
9+
// We don't insert on request alone — we'll log fully after response
10+
}
11+
12+
override fun logResponse(response: Response, tookMs: Double) {
13+
val request = response.request
14+
15+
val logEntry = HttpLogEntry(
16+
method = request.method,
17+
url = request.url.toString(),
18+
requestHeaders = request.headers.joinToString("\n") { "${it.first}: ${it.second}" },
19+
responseCode = response.code,
20+
responseHeaders = response.headers.joinToString("\n") { "${it.first}: ${it.second}" },
21+
durationMs = tookMs,
22+
timestamp = Instant.now(),
23+
)
24+
25+
repository.insert(logEntry)
26+
}
27+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package dev.shtanko.http
2+
3+
import java.time.Instant
4+
5+
data class HttpLogEntry(
6+
val id: Long? = null,
7+
val method: String,
8+
val url: String,
9+
val requestHeaders: String,
10+
val responseCode: Int,
11+
val responseHeaders: String,
12+
val durationMs: Double,
13+
val timestamp: Instant,
14+
)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package dev.shtanko.http
2+
3+
import java.sql.Connection
4+
import java.sql.ResultSet
5+
import java.time.Instant
6+
7+
class HttpLogRepository(private val connection: Connection) {
8+
9+
companion object {
10+
private const val CREATE_TABLE_SQL = """
11+
CREATE TABLE IF NOT EXISTS http_logs (
12+
id INTEGER PRIMARY KEY AUTOINCREMENT,
13+
method TEXT NOT NULL,
14+
url TEXT NOT NULL,
15+
request_headers TEXT NOT NULL,
16+
response_code INTEGER NOT NULL,
17+
response_headers TEXT NOT NULL,
18+
duration_ms REAL NOT NULL,
19+
timestamp TEXT NOT NULL
20+
)
21+
"""
22+
23+
private const val INSERT_SQL = """
24+
INSERT INTO http_logs (
25+
method, url, request_headers, response_code,
26+
response_headers, duration_ms, timestamp
27+
) VALUES (?, ?, ?, ?, ?, ?, ?)
28+
"""
29+
30+
private const val SELECT_ALL_SQL = "SELECT * FROM http_logs ORDER BY timestamp DESC"
31+
}
32+
33+
fun init() {
34+
connection.createStatement().use { stmt ->
35+
stmt.execute(CREATE_TABLE_SQL)
36+
}
37+
}
38+
39+
fun insert(log: HttpLogEntry) {
40+
connection.prepareStatement(INSERT_SQL).use { pstmt ->
41+
pstmt.apply {
42+
setString(1, log.method)
43+
setString(2, log.url)
44+
setString(3, log.requestHeaders)
45+
setInt(4, log.responseCode)
46+
setString(5, log.responseHeaders)
47+
setDouble(6, log.durationMs)
48+
setString(7, log.timestamp.toString())
49+
}.executeUpdate()
50+
}
51+
}
52+
53+
fun getAll(): List<HttpLogEntry> {
54+
return connection.createStatement().use { stmt ->
55+
stmt.executeQuery(SELECT_ALL_SQL).use { rs ->
56+
generateSequence { if (rs.next()) rs.toHttpLogEntry() else null }
57+
.toList()
58+
}
59+
}
60+
}
61+
62+
fun getById(id: Long): HttpLogEntry? {
63+
return connection.prepareStatement("SELECT * FROM http_logs WHERE id = ?").use { pstmt ->
64+
pstmt.setLong(1, id)
65+
pstmt.executeQuery().use { rs ->
66+
if (rs.next()) rs.toHttpLogEntry() else null
67+
}
68+
}
69+
}
70+
71+
fun getByUrlPattern(urlPattern: String): List<HttpLogEntry> {
72+
return connection.prepareStatement("SELECT * FROM http_logs WHERE url LIKE ? ORDER BY timestamp DESC")
73+
.use { pstmt ->
74+
pstmt.setString(1, "%$urlPattern%")
75+
pstmt.executeQuery().use { rs ->
76+
generateSequence { if (rs.next()) rs.toHttpLogEntry() else null }
77+
.toList()
78+
}
79+
}
80+
}
81+
82+
fun deleteOlderThan(timestamp: Instant): Int {
83+
return connection.prepareStatement("DELETE FROM http_logs WHERE timestamp < ?").use { pstmt ->
84+
pstmt.setString(1, timestamp.toString())
85+
pstmt.executeUpdate()
86+
}
87+
}
88+
89+
fun count(): Long {
90+
return connection.createStatement().use { stmt ->
91+
stmt.executeQuery("SELECT COUNT(*) as count FROM http_logs").use { rs ->
92+
if (rs.next()) rs.getLong("count") else 0
93+
}
94+
}
95+
}
96+
97+
private fun ResultSet.toHttpLogEntry(): HttpLogEntry {
98+
return HttpLogEntry(
99+
id = getLong("id"),
100+
method = getString("method"),
101+
url = getString("url"),
102+
requestHeaders = getString("request_headers"),
103+
responseCode = getInt("response_code"),
104+
responseHeaders = getString("response_headers"),
105+
durationMs = getDouble("duration_ms"),
106+
timestamp = Instant.parse(getString("timestamp")),
107+
)
108+
}
109+
}

0 commit comments

Comments
 (0)