Skip to content

v0.6.0: split into core/jpa/r2dbc/mybatis, migrate to Gradle#1

Merged
jlc488 merged 9 commits into
masterfrom
v0.6.0
May 20, 2026
Merged

v0.6.0: split into core/jpa/r2dbc/mybatis, migrate to Gradle#1
jlc488 merged 9 commits into
masterfrom
v0.6.0

Conversation

@jlc488
Copy link
Copy Markdown
Collaborator

@jlc488 jlc488 commented May 20, 2026

Summary

  • Splits api-log-spring-boot-starter into four publishable artifactsapi-log-core (backend-agnostic) + one of api-log-jpa / api-log-r2dbc / api-log-mybatis. Consumers pick the persistence backend they actually use.
  • Migrates the build from Maven to Gradle 8.10 with Vanniktech maven-publish per module — same convention as easy-paging-spring-boot-starter.
  • Adds the new ApiLogWriter SPI in :core. Each backend module registers exactly one implementation; the core listener routes events through it.
  • Adds R2DBC + MyBatis backends alongside the existing JPA flow.

Artifact map

Coordinate What it provides
kr.devslab:api-log-core Events, SPI, async listener, HTTP client utilities, shared V1.0__create_api_log.sql
kr.devslab:api-log-jpa JPA + Hibernate writer (drop-in for v0.5.x) + Flyway hook
kr.devslab:api-log-r2dbc Reactive R2DBC writer (DatabaseClient-based), pure-reactive schema initializer — no JDBC pull-in
kr.devslab:api-log-mybatis @Mapper writer with ::jsonb cast in the @Insert SQL

Package renames (BREAKING)

  • kr.devslab.apilog.model.dto.*kr.devslab.apilog.dto.*
  • kr.devslab.apilog.model.ApiLogEntitykr.devslab.apilog.jpa.model.ApiLogEntity
  • kr.devslab.apilog.repository.ApiLogRepositorykr.devslab.apilog.jpa.repository.ApiLogRepository
  • kr.devslab.apilog.service.ApiLogServicekr.devslab.apilog.jpa.writer.JpaApiLogWriter (now an ApiLogWriter impl)

Migration mapping in docs/changelog.md § 0.6.0.

Other fixes & docs

  • V1.0__create_api_log.sql now uses IF NOT EXISTS on both CREATE TABLE and CREATE INDEX — idempotent under BUILTIN mode.
  • CI workflow rewritten for Gradle; per-module Jacoco upload glob.
  • Release workflow now uses publishAndReleaseToMavenCentral and ships all four artifacts in one job.
  • New backend guides (en+ko): docs/guides/jpa-backend.md, r2dbc-backend.md, mybatis-backend.md. mkdocs.yml nav updated.
  • Install table + architecture diagram refreshed in README.md / README.ko.md and the installation/quickstart pages.

Test plan

  • CI: ./gradlew build jacocoTestReport green on Ubuntu runner (all 4 modules, including Testcontainers PostgreSQL integration tests for :jpa, :r2dbc, :mybatis).
  • Codecov upload picks up coverage from all four */build/reports/jacoco/test/jacocoTestReport.xml files.
  • docs workflow: mkdocs build --strict succeeds with the new backend guides in the nav and their *.ko.md siblings.
  • (after merge) Tag v0.6.0 on master → release workflow publishes api-log-core, api-log-jpa, api-log-r2dbc, api-log-mybatis to Maven Central in one shot.

Local verification: all four modules compile (main + test), 23 :core unit tests + :jpa JpaApiLogWriterTest mock 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.

jlc488 added 9 commits May 21, 2026 02:41
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.
@jlc488 jlc488 merged commit 81c2ff9 into master May 20, 2026
1 check passed
@jlc488 jlc488 deleted the v0.6.0 branch May 20, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant