Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ Framework integration points are now instance-scoped (#198), and the Ktor integr
- `OtelDatabaseObservationConvention` (storm-micrometer): opt-in OpenTelemetry database client semantic conventions on query observations — `db.system.name` (derived from the JDBC URL), `db.operation.name`, and `db.query.text` (placeholders only) alongside the `storm.*` key values, so Storm queries surface in the database tooling of OTLP-capable observability backends. Activated with `storm.observations.semantic-conventions=otel` in the Spring Boot starters (database product detected from the DataSource) or by registering the convention in the Ktor dependency container.
- Trace context in SQL statements: the `SqlCommenter` hook (template builder, all language stacks) appends per-execution comment content to statements after all processing and caching; `TraceContextSqlCommenter` (storm-micrometer) renders the current span as a sqlcommenter-style W3C `traceparent` comment, correlating database-side diagnostics such as slow query logs with traces. Opt-in everywhere — `storm.tracing.sql-comments=true` in the Spring Boot starters (with a `Tracer` present), the `sqlCommenter` slot in the Ktor plugin — since a per-execution comment defeats prepared statement caching. Hostile content is rejected (comment terminator, semicolons), and the emitted comment is padded so MySQL/MariaDB executable-comment and optimizer-hint markers are never interpreted.
- Transactions are observed alongside queries: every physical transaction (outermost block or `REQUIRES_NEW`, blocking, suspend, and Spring-bridged alike) reports as a `storm.transaction` Micrometer Observation with duration, outcome (`committed`/`rolled_back`), propagation, and read-only key values; joined blocks are not double-counted. The `QueryObserver` SPI gains a default no-op transaction hook, so existing observers and wiring are unaffected.
- `@DataStormTest` provides an embedded test database on Spring Boot 4 as well: a fallback in the slice activates when Boot's own test-database replacement (relocated to spring-boot-jdbc-test-autoconfigure in Boot 4) is absent, so the slice behaves identically on both generations; `spring.test.database.replace=none` opts out for Testcontainers setups.
- Trace-context SQL comments gain a sampled-only mode: `TraceContextSqlCommenter(tracer, onlySampled)` and `storm.tracing.sql-comments=sampled` comment only statements of sampled traces, aligning the prepared-statement-caching cost with the correlation benefit; unknown property values fail fast.
- `@EnableStormRepositories(basePackages = ...)` (storm-spring, usable from both language stacks): switches on repository scanning in plain Spring applications, mirroring `@EnableJpaRepositories`; without packages, the annotated class's package is scanned. In Spring Boot the annotation doubles as the explicit override: the auto-configured scanning backs off. The `RepositoryBeanFactoryPostProcessor` adapters also gain configuring constructors (base packages, template bean name, prefix), so multi-template applications define one bean per repository set instead of one subclass.
- `@DataStormTest` test slice in the new storm-spring-boot-test-autoconfigure module, the counterpart of `@DataJpaTest`: starts only the `DataSource`, Storm's auto-configuration, and SQL initialization (plus Flyway/Liquibase when present), keeps regular components out, replaces the data source with an embedded database on Spring Boot 3 (opt out via `spring.test.database.replace=none` for Testcontainers `@ServiceConnection` setups), and rolls back a per-test transaction. One module serves both starters and both Spring Boot 3 and 4: the slice imports the starters' identically named auto-configuration classes, lists relocated platform classes as optional imports, and registers its type exclusion through a context customizer instead of the relocated `@TypeExcludeFilters`.

Expand Down
2 changes: 1 addition & 1 deletion docs/ktor-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ dependencies {
}
```

With tracing in place, the `sqlCommenter` slot appends the current trace context to every statement as a sqlcommenter-style comment, correlating database-side diagnostics such as slow query logs back to the trace. Opt-in: a per-execution comment defeats prepared statement caching.
With tracing in place, the `sqlCommenter` slot appends the current trace context to statements as a sqlcommenter-style comment, correlating database-side diagnostics such as slow query logs back to the trace. Opt-in: a per-execution comment defeats prepared statement caching; `TraceContextSqlCommenter(tracer, onlySampled = true)` limits the comments to sampled traces.

```kotlin
install(Storm) {
Expand Down
8 changes: 5 additions & 3 deletions docs/spring-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,13 +636,15 @@ storm:

Customization follows the usual back-off rules: contribute an `ObservationConvention<StormQueryObservationContext>` bean to override the naming and key values (it wins over the property), define your own `QueryObserver` bean to replace the binding entirely, or disable the observation at the registry level with `management.observations.enable.storm.query=false`.

With tracing in place, `storm.tracing.sql-comments: true` additionally appends the current trace context to every statement as a sqlcommenter-style comment (`/*traceparent='00-…'*/`), so database-side diagnostics — MariaDB's slow query log, `pg_stat_activity` — correlate directly back to the trace that issued the statement. This is deliberately opt-in: a per-execution comment changes the statement text on every call, which defeats driver-side and server-side prepared statement caching. Enable it where the correlation is worth that trade-off.
With tracing in place, `storm.tracing.sql-comments` additionally appends the current trace context to statements as a sqlcommenter-style comment (`/*traceparent='00-…'*/`), so database-side diagnostics — MariaDB's slow query log, `pg_stat_activity` — correlate directly back to the trace that issued the statement. `true` comments every statement inside a span; `sampled` comments only statements of sampled traces, which is the recommended mode when the sampling probability is below 1.0. This is deliberately opt-in: a per-execution comment changes the statement text on every call, which defeats driver-side and server-side prepared statement caching.

One composition warning for applications that wire the integration beans themselves: define beans like the `ExceptionMapper`, `QueryObserver`, or `SqlCommenter` unconditionally in your `@Configuration` classes. A `@ConditionalOnBean` condition in a user configuration evaluates before auto-configurations contribute their beans (such as the `Tracer`), so it fails silently and the integration stays dormant while looking enabled.

## Testing with @DataStormTest

`@DataStormTest` (from `storm-spring-boot-test-autoconfigure`, test scope) is the Storm test slice, the counterpart of `@DataJpaTest`: it starts only the `DataSource`, Storm's auto-configuration (template, repository scanning, transaction integration, schema validation, exception translation), and SQL initialization, plus Flyway or Liquibase when present. It complements [`@StormTest`](testing.md), which tests data logic without a Spring context: use `@StormTest` for fast query-level tests, and the slice when the test should see what production Spring code sees — injected repository beans, translated exceptions, Spring-managed transactions, and your `storm.*` configuration. Regular `@Component`, `@Service`, and `@Controller` beans stay out of the context. Each test method runs in a transaction that is rolled back afterwards, so tests cannot see each other's writes.

On Spring Boot 3 the application's `DataSource` is replaced with an embedded in-memory database by default; put a `schema.sql` (and optionally `data.sql`) on the test classpath to initialize it, or let Flyway or Liquibase (included in the slice) create the schema as in production. On Spring Boot 4 the replacement activates when the `spring-boot-jdbc-test-autoconfigure` artifact is present; without it, point the slice at a database with the `properties` attribute:
The application's `DataSource` is replaced with an embedded in-memory database by default, on Spring Boot 3 and 4 alike (the slice ships a fallback for Boot 4's relocated test-database support); put a `schema.sql` (and optionally `data.sql`) on the test classpath to initialize it, or let Flyway or Liquibase (included in the slice) create the schema as in production:

```kotlin
@DataStormTest
Expand All @@ -662,7 +664,7 @@ class VisitRepositoryTest(
}
```

To run against a real database instead, disable the replacement (`spring.test.database.replace=none` on Spring Boot 3; the default without the test-database artifact on Spring Boot 4) and hand the slice a Testcontainers-managed database through `@ServiceConnection`:
To run against a real database instead, disable the replacement with `spring.test.database.replace=none` and hand the slice a Testcontainers-managed database through `@ServiceConnection`:

```kotlin
@DataStormTest(properties = ["spring.test.database.replace=none"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,30 @@
public class TraceContextSqlCommenter implements SqlCommenter {

private final Tracer tracer;
private final boolean onlySampled;

/**
* Creates a commenter reading the current span from the given tracer.
* Creates a commenter that comments every statement executed inside a span.
*
* @param tracer the tracer providing the current trace context.
*/
public TraceContextSqlCommenter(@Nonnull Tracer tracer) {
this(tracer, false);
}

/**
* Creates a commenter that optionally comments only statements of sampled traces.
*
* <p>With sampling below 1.0, commenting every statement pays the prepared statement caching cost for
* comments whose trace is mostly not exported; sampled-only mode aligns the cost with the correlation
* benefit.</p>
*
* @param tracer the tracer providing the current trace context.
* @param onlySampled whether to comment only when the current span is sampled.
*/
public TraceContextSqlCommenter(@Nonnull Tracer tracer, boolean onlySampled) {
this.tracer = requireNonNull(tracer, "tracer");
this.onlySampled = onlySampled;
}

@Override
Expand All @@ -58,6 +74,9 @@ public Optional<String> comment() {
return Optional.empty();
}
TraceContext context = span.context();
if (onlySampled && !Boolean.TRUE.equals(context.sampled())) {
return Optional.empty();
}
String flags = Boolean.TRUE.equals(context.sampled()) ? "01" : "00";
return Optional.of("traceparent='00-%s-%s-%s'".formatted(context.traceId(), context.spanId(), flags));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@ public void executableCommentMarkersAreNeutralizedByPadding() {
statements.toString());
}

@Test
public void sampledOnlyModeSkipsUnsampledSpans() {
var tracer = new SimpleTracer();
var sampledOnly = new TraceContextSqlCommenter(tracer, true);
var always = new TraceContextSqlCommenter(tracer);
var span = tracer.nextSpan().start();
try (var ignored = tracer.withSpan(span)) {
// SimpleTracer spans are not sampled: the sampled-only commenter stays silent while the
// default commenter still renders the trace identity.
assertEquals(Optional.empty(), sampledOnly.comment());
assertTrue(always.comment().isPresent());
} finally {
span.end();
}
}

@Test
public void traceContextCommenterRendersTraceparent() {
var tracer = new SimpleTracer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,27 @@ void unknownSemanticConventionsValueFailsFast() {
.run(context -> assertThat(context).hasFailed());
}

@Test
void traceSqlCommentsSupportSampledModeAndFailFastOnUnknownValues() {
contextRunner
.withPropertyValues(
"spring.datasource.url=jdbc:h2:mem:sqlCommentsSampledTest;DB_CLOSE_DELAY=-1",
"spring.datasource.driver-class-name=org.h2.Driver",
"storm.tracing.sql-comments=sampled"
)
.withBean(io.micrometer.tracing.Tracer.class, () -> org.mockito.Mockito.mock(io.micrometer.tracing.Tracer.class))
.run(context -> assertThat(context).getBean(st.orm.core.spi.SqlCommenter.class)
.isInstanceOf(st.orm.micrometer.TraceContextSqlCommenter.class));
contextRunner
.withPropertyValues(
"spring.datasource.url=jdbc:h2:mem:sqlCommentsBogusTest;DB_CLOSE_DELAY=-1",
"spring.datasource.driver-class-name=org.h2.Driver",
"storm.tracing.sql-comments=bogus"
)
.withBean(io.micrometer.tracing.Tracer.class, () -> org.mockito.Mockito.mock(io.micrometer.tracing.Tracer.class))
.run(context -> assertThat(context).hasFailed());
}

@Test
void traceSqlCommentsAreOptIn() {
// Off by default even with a Tracer present: a per-execution comment defeats prepared statement
Expand Down
4 changes: 4 additions & 0 deletions storm-spring-boot-test-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2024 - 2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package st.orm.spring.boot.test;

import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;

/**
* Fallback embedded test database for {@link DataStormTest @DataStormTest} slices on Spring Boot 4.
*
* <p>Spring Boot 3 replaces the application's {@code DataSource} with an embedded database through its own
* {@code TestDatabaseAutoConfiguration}, which the slice imports. Spring Boot 4 hosts that behavior in the
* separate {@code spring-boot-jdbc-test-autoconfigure} artifact; when it is absent, this fallback provides
* an embedded H2 database so the slice behaves identically on both generations.</p>
*
* <p>The fallback stays inert whenever Boot's own replacement is present (either location on the classpath),
* when H2 is not on the test classpath, or when {@code spring.test.database.replace=none} requests the
* configured database, such as a Testcontainers-managed one.</p>
*
* @since 1.13
*/
@AutoConfiguration(
beforeName = {
// Spring Boot 3.x location.
"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration",
// Spring Boot 4.x location.
"org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration",
})
@ConditionalOnMissingClass({
// Boot's own replacement wins by presence; both its locations checked.
"org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration",
"org.springframework.boot.jdbc.test.autoconfigure.TestDatabaseAutoConfiguration",
})
@ConditionalOnClass(name = "org.h2.Driver")
@Conditional(DataStormTestDatabaseAutoConfiguration.ReplacementRequested.class)
public class DataStormTestDatabaseAutoConfiguration {

/**
* Provides the embedded test database, taking precedence over the application's configured data source
* through the DataSource auto-configuration's own back-off.
*/
@Bean
@ConditionalOnMissingBean(DataSource.class)
public DataSource dataStormTestDatabase() {
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.build();
}

/**
* Matches unless {@code spring.test.database.replace=none} requests the configured database.
*/
static class ReplacementRequested extends NoneNestedConditions {

ReplacementRequested() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}

@ConditionalOnProperty(name = "spring.test.database.replace", havingValue = "none")
static class ReplaceNone {
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
st.orm.spring.boot.test.DataStormTestDatabaseAutoConfiguration
optional:org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
optional:org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration
optional:org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,16 +245,18 @@ public static class Validation {
public static class Tracing {

/**
* Whether the current trace context is appended to SQL statements as a sqlcommenter-style comment.
* Defaults to false: a per-execution comment defeats prepared statement caching.
* Whether the current trace context is appended to SQL statements as a sqlcommenter-style comment:
* "true" comments every statement inside a span, "sampled" comments only statements of sampled
* traces, "false" (default) disables the comments. A per-execution comment defeats prepared
* statement caching; prefer "sampled" when the sampling probability is below 1.0.
*/
private Boolean sqlComments;
private String sqlComments;

/** Returns whether trace context SQL comments are enabled. */
public Boolean getSqlComments() { return sqlComments; }
/** Returns the trace context SQL comment mode. */
public String getSqlComments() { return sqlComments; }

/** Sets whether trace context SQL comments are enabled. */
public void setSqlComments(Boolean sqlComments) { this.sqlComments = sqlComments; }
/** Sets the trace context SQL comment mode. */
public void setSqlComments(String sqlComments) { this.sqlComments = sqlComments; }
}

/** Query observation configuration. */
Expand Down
Loading
Loading