Conversation
Splits the single api-log-spring-boot-starter into four publishable artifacts so consumers pick the persistence backend they actually use: kr.devslab:api-log-core events, ApiLogWriter SPI, listener, HTTP utils kr.devslab:api-log-jpa JPA + Hibernate (drop-in for v0.5.x users) kr.devslab:api-log-r2dbc reactive R2DBC, no JDBC dependency kr.devslab:api-log-mybatis MyBatis mapper backend Build system migrates from Maven to Gradle 8.10 with Vanniktech maven-publish per module, matching the easy-paging-spring-boot-starter convention. pom.xml / mvnw / .mvn removed; gradle wrapper bundled. New ApiLogWriter SPI in :core. Each backend module registers exactly one implementation, and the core listener routes events through it. HttpErrorExtractor and PayloadJsonMapper helpers are also lifted into :core so all three writers share one HTTP-status-extraction path and one JSON canonicalization path. Package renames as part of the split: - kr.devslab.apilog.model.dto.* -> kr.devslab.apilog.dto.* - kr.devslab.apilog.model.ApiLogEntity -> kr.devslab.apilog.jpa.model.ApiLogEntity - kr.devslab.apilog.repository.* -> kr.devslab.apilog.jpa.repository.* - kr.devslab.apilog.service.ApiLogService -> kr.devslab.apilog.jpa.writer.JpaApiLogWriter Fixed: V1.0__create_api_log.sql now uses IF NOT EXISTS on both CREATE TABLE and CREATE INDEX -- idempotent across boots under BUILTIN mode (previously could fail on second boot if Hibernate's ddl-auto wasn't catching it). CI workflow rewritten for Gradle with per-module Jacoco upload glob. Release workflow now publishes via Vanniktech's publishAndReleaseToMavenCentral, releasing all four artifacts in one go. Docs: new install table with the backend choice, three new backend guides (en+ko), v0.6.0 changelog entry with migration mapping, mkdocs nav updated. Local verification: all four modules compile (main + test), the 23 unit tests in :core plus the JPA writer mock tests all pass. Testcontainers integration tests (jpa/r2dbc/mybatis) require a Docker daemon socket the CI Ubuntu runner has by default; on Windows + Docker Desktop they need the socket-access toggle and will be exercised through CI on this PR.
CI exposed what the local single-jar tests masked: same-class @ConditionalOnBean evaluation happens before the sibling @bean declarations are registered, so PayloadJsonMapper / DatabaseClient / ApiLogRepository / ApiLogMapper / ApiLogWriter beans never satisfied the guards and the writer beans were silently dropped. Replaced the @ConditionalOnBean guards with plain constructor injection — Spring DI resolves the dependencies lazily, which is the documented "right" pattern for in-class dependencies. Added @autoConfiguration(after = ...) hints so each auto-config evaluates after the Spring Boot autoconfig that registers its prerequisites: - ApiLogCoreAutoConfiguration -> after JacksonAutoConfiguration - ApiLogR2dbcAutoConfiguration -> after R2dbcAutoConfiguration - ApiLogMybatisAutoConfiguration -> after MybatisAutoConfiguration A missing backend artifact now fails fast at context refresh with a clear NoSuchBeanDefinitionException for ApiLogWriter, which is more actionable than silently dropping events. Local: :core 23 unit tests pass; all four modules compile.
…fe assertions Four problems exposed by the previous CI run, now fixed: 1. ApiEventListener handlers re-gained @async. Without it the event listener ran on the publisher's thread -- a reactor I/O thread in the WebClient path -- so the R2DBC writer's subscribe() ended up sharing CPU with the caller's reactive chain and never got to actually emit. 2. R2dbcApiLogWriter now pins its subscription to Schedulers.boundedElastic() and adds doOnSuccess / doOnError logging. Fire-and-forget is preserved (no .block() -- that would defeat the point of having a reactive backend); the explicit scheduler just guarantees the insert runs on a worker thread that isn't competing with the test's polling loop or a single-core CI runner. 3. Writer-identity assertions in three integration tests switched from getSimpleName() == "Foo" to getName().contains("Foo"). JPA and MyBatis writers are wrapped in a CGLIB proxy because of @transactional, so the runtime class name is `Foo$$SpringCGLIB$$0` -- the substring check survives the proxy. 4. :jpa test deps now include spring-boot-starter-webflux, so ConfigurationTest.allAutoConfigurationsAreLoaded() can verify ReactiveApiClientAutoConfiguration also activates -- matches the reality of a mixed Servlet+WebFlux consumer. Local: :core 23 unit tests still pass; all four modules compile.
…2dbc CI is still red on the r2dbc + mybatis integration tests; both report 0 rows inserted but no error. This commit narrows the two hypotheses so the next CI run will pinpoint each one separately. MyBatis: temporarily remove @transactional(REQUIRES_NEW) from MybatisApiLogWriter. mybatis-spring's SpringManagedTransaction then runs the mapper call against the connection's auto-commit, so the insert lands immediately and is visible to subsequent reads. If the four mybatis integration tests turn green, the wrapping transaction wasn't committing for some reason and a follow-up commit will reinstate REQUIRES_NEW with the actual fix. If they stay red, the problem is elsewhere (mapper wiring, JDBC connection pool, etc.). R2DBC: bump the Awaitility timeouts in R2dbcApiLogWriterIntegrationTest from 5s to 15s (CI runners can be slow on first Reactor scheduler use), and raise the writer's doOnSubscribe / doOnSuccess / doOnError logs from DEBUG to INFO so the CI run reveals whether the chain ever subscribed in the first place. If neither the "subscribe" nor the "insert ok"/"insert failed" line shows, the .subscribe() call wasn't triggering the chain and we need to look at subscriber lifecycle (probably Disposable GC) -- if "subscribe" lands but "insert ok" doesn't, the boundedElastic worker is starved and we need a different scheduler hand-off.
…ecycle Previous run's diagnostic output was invisible because every module's test task had showStandardStreams = false -- application log lines never reached the Gradle console. Flipped to true (temporary, will go back to false once the integration tests are stable). Also added INFO-level "MyBatis insert X / X done" pairs around each mapper.insert() call so the next CI run can prove whether the insert ran and whether mapper.insert() returned without throwing. The intriguing data point from the previous run -- writeError_marksRetry PASSED, writer_isWiredFromMybatisBackend PASSED, but the other three FAILED with empty rows -- suggests the inserts that ran did commit (@transactional is off in this commit, so it's straight auto-commit JDBC). The mystery is why the same code path produces a row for one test and zero rows for the others.
…ibility
R2DBC: root cause was a wrong assumption in v0.6.0's R2dbcApiLogWriter
- I'd documented "PostgreSQL R2DBC driver does the TEXT -> JSONB
implicit cast", but it actually doesn't. r2dbc-postgresql binds CLOB
params as text and the server rejects the insert with
column "payload" is of type jsonb but expression is of type text
Fixed by adding explicit `::jsonb` casts on payload / response /
error_message in the INSERT, matching what the mybatis backend's
mapper already does. No codec change; the bind path stays
R2dbcType.CLOB.
MyBatis: previous diagnostics confirmed mapper.insert() returned
without throwing and the row got a BIGSERIAL id, yet
mapper.findByRequestId() came back empty for three of the four tests
- the fourth ("retry") passed identically. That points at a SELECT
visibility issue, not a write failure. Added diagFindByRequestId()
that runs the mapper's SELECT alongside two raw JdbcTemplate counts
(by request_id and total rows) so the next run will tell whether
(a) mapper-only blindness (mybatis session/cache issue) or
(b) global emptiness (something is rolling back / deleting between
the writer call and the assertion).
The R2DBC SQL fix from the previous commit produced visibly correct
INSERTs (r2dbc subscribe + insert ok rows=1; mybatis mapperRows=1
jdbcRows=1 jdbcTotal=1), but every JSON-content assertion failed
with shapes like
Expecting actual: "{\"amount\": 100}" to contain: "\"amount\":100"
PostgreSQL stores JSONB in canonical form (one space after each colon)
and the `column::text` SELECT returns that canonical form. The tests
were written against the input shape (no space) rather than the
stored shape (with space) -- false alarm.
Updated eight assertions across the r2dbc and mybatis integration
tests to expect the canonical form. The JPA backend doesn't need the
same change because Hibernate maps payload as JsonNode and the test
asserts via .get("user").get("name") rather than substring on the
serialized text.
This should be the last red light. The four R2DBC integration tests
and three mybatis tests that were failing on the payload/response/
error_message substring should now all pass on their already-
inserted rows.
The previous five iterations sprinkled diagnostic logging and
loosened a few production knobs to get to the root cause. Now that
all 44 tests pass on CI we undo the temporary noise:
- showStandardStreams = false in all four modules' test config
(Gradle console no longer dumps the entire Spring Boot startup).
- MybatisApiLogWriter regains @transactional(REQUIRES_NEW). The
earlier hypothesis that the wrapping transaction was the failure
cause turned out to be wrong -- the JSON canonical-form mismatch
was the actual cause -- so the matching contract with
JpaApiLogWriter (audit log committed regardless of caller tx
rollback) goes back in.
- MybatisApiLogWriter loses the INFO-level "insert / insert done"
log pairs.
- R2dbcApiLogWriter loses the doOnSubscribe line and downgrades
doOnSuccess from INFO to DEBUG; doOnError stays at ERROR so a
real production-time insert failure remains visible.
- MyBatis integration test drops the diagFindByRequestId helper +
BeforeEach println; calls mapper.findByRequestId directly.
- R2DBC integration test settles its Awaitility timeout at 10s
(down from 15s diagnostic margin, still 2x the original 5s for
CI cold-start headroom).
This is the commit that goes into v0.6.0 -- no temporary debugging
artifacts. Pushing to confirm CI stays green before merging.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
api-log-spring-boot-starterinto four publishable artifacts —api-log-core(backend-agnostic) + one ofapi-log-jpa/api-log-r2dbc/api-log-mybatis. Consumers pick the persistence backend they actually use.maven-publishper module — same convention aseasy-paging-spring-boot-starter.ApiLogWriterSPI in:core. Each backend module registers exactly one implementation; the core listener routes events through it.Artifact map
kr.devslab:api-log-coreV1.0__create_api_log.sqlkr.devslab:api-log-jpakr.devslab:api-log-r2dbcDatabaseClient-based), pure-reactive schema initializer — no JDBC pull-inkr.devslab:api-log-mybatis@Mapperwriter with::jsonbcast in the@InsertSQLPackage renames (BREAKING)
kr.devslab.apilog.model.dto.*→kr.devslab.apilog.dto.*kr.devslab.apilog.model.ApiLogEntity→kr.devslab.apilog.jpa.model.ApiLogEntitykr.devslab.apilog.repository.ApiLogRepository→kr.devslab.apilog.jpa.repository.ApiLogRepositorykr.devslab.apilog.service.ApiLogService→kr.devslab.apilog.jpa.writer.JpaApiLogWriter(now anApiLogWriterimpl)Migration mapping in
docs/changelog.md§ 0.6.0.Other fixes & docs
V1.0__create_api_log.sqlnow usesIF NOT EXISTSon bothCREATE TABLEandCREATE INDEX— idempotent under BUILTIN mode.publishAndReleaseToMavenCentraland ships all four artifacts in one job.docs/guides/jpa-backend.md,r2dbc-backend.md,mybatis-backend.md.mkdocs.ymlnav updated.README.md/README.ko.mdand the installation/quickstart pages.Test plan
./gradlew build jacocoTestReportgreen on Ubuntu runner (all 4 modules, including Testcontainers PostgreSQL integration tests for:jpa,:r2dbc,:mybatis).*/build/reports/jacoco/test/jacocoTestReport.xmlfiles.mkdocs build --strictsucceeds with the new backend guides in the nav and their*.ko.mdsiblings.v0.6.0onmaster→ release workflow publishesapi-log-core,api-log-jpa,api-log-r2dbc,api-log-mybatisto Maven Central in one shot.Local verification: all four modules compile (main + test), 23
:coreunit tests +:jpaJpaApiLogWriterTestmock tests all pass. Testcontainers integration tests blocked locally by Windows Docker Desktop's daemon socket access policy — CI Linux runner has the standard socket and will exercise them.