English · 한국어
Event-driven API call logging for Spring Boot. Async event pipeline with PostgreSQL JSONB storage.
📖 Documentation → api-log.devslab.kr
Logs every outbound HTTP call from your service into PostgreSQL — request, response, errors, retries — through a non-blocking event pipeline. Drop the starter on the classpath, and any HTTP call made via the bundled RestApiClientUtil (or events you publish yourself) gets persisted as JSONB without slowing the caller down.
@Service
public class UserService {
private final RestApiClientUtil api;
public UserService(RestApiClientUtil api) {
this.api = api;
}
public User createUser(User newUser) {
// The HTTP call is synchronous; logging fires async events behind the scenes.
return api.postSyncTyped("/api/users", newUser, User.class);
}
}Every call lands one or more rows in api_log:
- INITIATED — request fired
- SUCCESS / ERROR — terminal outcome with status code and payload
- RETRY_ERROR — emitted for each retry attempt that failed
Bodies are stored as JSONB, so you can query them with ->, ->>, and GIN indexes.
- Non-blocking — log writes happen on a separate thread, never on the request path
- PostgreSQL JSONB — request/response/error bodies preserved as queryable JSON
- Retry-aware schema —
RETRY_ERRORevent +retry_count/is_retrycolumns for tracking flaky calls. The listener also retries its own log writes 3× on transient DB failures. - Virtual Threads ready — designed for Java 21+ async with low memory footprint
- Drop-in starter — auto-configuration registers all beans behind
@ConditionalOnMissingBean
Caller code
↓
RestApiClientUtil (or your own HTTP client)
↓ publishEvent
ApplicationEventPublisher
↓ @EventListener (async)
ApiEventListener
↓
ApiLogService
↓
ApiLogRepository (JPA)
↓
PostgreSQL (api_log · JSONB columns)
<dependency>
<groupId>kr.devslab</groupId>
<artifactId>api-log-spring-boot-starter</artifactId>
<version>0.3.0</version>
</dependency>implementation("kr.devslab:api-log-spring-boot-starter:0.3.0")api:
log:
enabled: true # default — set to false to disable the whole infrastructure
schema:
management: builtin # default — see "Schema" belowYou bring your own:
DataSourcepointing at a PostgreSQL databaseObjectMapperbean (Spring Boot's auto-config is fine)
The api_log table is created for you on first boot — no other setup needed.
api.log.schema.management selects how the api_log table is provisioned:
builtin(default) — the starter runsCREATE TABLE IF NOT EXISTSon startup. Idempotent, requires no migration tool. Just works.flyway— register with the consumer's Flyway flow (flyway_schema_historytracks the migration). Requiresflyway-coreon the classpath.none— the starter does not touch the schema. Apply the DDL yourself via Liquibase / manualpsql/ your own flow. See the Schema reference for the SQL.
Full installation guide: api-log.devslab.kr/getting-started/installation.
// GET
ApiResponse r = api.getSync("/api/users/1");
User user = api.getSyncTyped("/api/users/1", User.class);
// POST
ApiResponse r = api.postSync("/api/users", payload);
User created = api.postSyncTyped("/api/users", payload, User.class);
// Async
CompletableFuture<ApiResponse> f = api.postAsync("/api/users", payload);Bring your own HTTP client and only use the logging side:
@Service
@RequiredArgsConstructor
public class MyClient {
private final ApplicationEventPublisher publisher;
public void call() {
ApiRequest req = ApiRequest.builder()
.endpoint("/external/users")
.payload("{\"name\":\"John\"}")
.build();
publisher.publishEvent(new ApiCallInitiatedEvent(this, req));
try {
ApiResponse res = doHttp(req); // your HTTP call
publisher.publishEvent(new ApiCallSuccessEvent(this, req, res));
} catch (Exception e) {
publisher.publishEvent(new ApiCallErrorEvent(this, req, e, 0, false));
}
}
}| Column | Type | Notes |
|---|---|---|
id |
BIGSERIAL | PK |
event_type |
VARCHAR(50) | INITIATED, SUCCESS, ERROR, RETRY_ERROR |
request_id |
VARCHAR(36) | UUID correlation id |
endpoint |
VARCHAR(255) | Target URL |
payload |
JSONB | Request body |
response |
JSONB | Response body |
error_message |
JSONB | Error details (if any) |
status_code |
INTEGER | HTTP status |
timestamp |
TIMESTAMP | When the event fired |
retry_count |
INTEGER | 0 for the initial attempt |
is_retry |
BOOLEAN | true for retry attempts |
CREATE INDEX idx_api_log_endpoint ON api_log (endpoint);
CREATE INDEX idx_api_log_timestamp ON api_log (timestamp DESC);
CREATE INDEX idx_api_log_payload_gin ON api_log USING GIN (payload);
CREATE INDEX idx_api_log_response_gin ON api_log USING GIN (response);-- Error rate per endpoint, last 1 hour
SELECT endpoint,
COUNT(*) FILTER (WHERE event_type = 'ERROR') * 100.0 / COUNT(*) AS error_rate
FROM api_log
WHERE timestamp > NOW() - INTERVAL '1 hour'
GROUP BY endpoint
HAVING COUNT(*) > 10
ORDER BY error_rate DESC;- Java 21+
- Spring Boot 3.5+
- PostgreSQL 15+ (for JSONB)
Apache License 2.0 — see LICENSE and NOTICE.
Built by Devslab · Part of the DevsLab open-source toolkit.