From 813a2668fcaaba2801d7ee0b475671cae9fc3a70 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Fri, 27 Oct 2023 15:12:37 -0500 Subject: [PATCH 1/9] Add support for using env vars to override cassandra datastax driver configuration properties. fixes #240 --- cassandra/build.gradle | 6 +++ .../cassandra/CassandraSessionFactory.java | 49 +++++++++++++++--- .../metrics/CassandraMetricsSpec.groovy | 50 ++++++++++++++++++- gradle/libs.versions.toml | 2 + 4 files changed, 97 insertions(+), 10 deletions(-) diff --git a/cassandra/build.gradle b/cassandra/build.gradle index e9035eba..f245eba0 100644 --- a/cassandra/build.gradle +++ b/cassandra/build.gradle @@ -15,4 +15,10 @@ dependencies { testImplementation(libs.testcontainers.cassandra) testImplementation(mn.micronaut.management) testImplementation(mnMicrometer.micronaut.micrometer.core) + testImplementation(libs.system.stubs) +} + +tasks.withType(Test) { + // this is needed by libs.system.stubs + jvmArgs = ["--add-opens", "java.base/java.util=ALL-UNNAMED", "--add-opens", "java.base/java.lang=ALL-UNNAMED"] } diff --git a/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java b/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java index 7edf61c3..7a74d423 100644 --- a/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java +++ b/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java @@ -21,6 +21,9 @@ import com.typesafe.config.ConfigFactory; import io.micronaut.context.annotation.EachBean; import io.micronaut.context.annotation.Factory; +import io.micronaut.context.env.Environment; +import io.micronaut.context.env.MapPropertySource; +import io.micronaut.context.env.PropertySource; import io.micronaut.core.naming.conventions.StringConvention; import io.micronaut.core.value.PropertyResolver; import jakarta.annotation.PreDestroy; @@ -30,6 +33,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; /** @@ -59,20 +63,16 @@ public CassandraSessionFactory(PropertyResolver propertyResolver) { * Creates the {@link CqlSessionBuilder} bean for the given configuration. * * @param configuration The cassandra configuration bean + * @param environment The environment with possibly overriding environment variables * @return A {@link CqlSession} bean */ @EachBean(CassandraConfiguration.class) - public CqlSessionBuilder session(CassandraConfiguration configuration) { + public CqlSessionBuilder session(CassandraConfiguration configuration, Environment environment) { try { return CqlSession.builder().withConfigLoader(new DefaultDriverConfigLoader(() -> { ConfigFactory.invalidateCaches(); - String prefix = configuration.getName(); - Map properties = this.resolver.getProperties(CassandraConfiguration.PREFIX + "." + prefix, StringConvention.RAW); - // translate indexed properties for list values from Micronaut array index notation (i.e. foo[0]=bar) - // to Datastax driver decimal notation (i.e. foo.0=bar) - properties = properties.entrySet().stream() - .collect((Collectors.toMap(e -> e.getKey().replaceAll("\\[(\\d+)]", ".$1"), Map.Entry::getValue))); - return ConfigFactory.parseMap(properties).withFallback(ConfigFactory.load().getConfig(DefaultDriverConfigLoader.DEFAULT_ROOT_PATH)); + var dataStaxProperties = dataStaxProperties(configuration.getName(), environment); + return ConfigFactory.parseMap(dataStaxProperties).withFallback(ConfigFactory.load().getConfig(DefaultDriverConfigLoader.DEFAULT_ROOT_PATH)); })); } catch (Exception e) { LOG.error(String.format("Failed to instantiate CQL session: %s", e.getMessage()), e); @@ -80,6 +80,39 @@ public CqlSessionBuilder session(CassandraConfiguration configuration) { } } + /* + Cassandra's Datastax java driver is rather intolerant of Micronaut configuration properties, + so they need to be dealt with specially to pass to the driver. + + TODO: document this and add information to user documentation + TODO: add support for + */ + private Map dataStaxProperties(String prefix, Environment environment) { + final Map configProperties = this.resolver + .getProperties(CassandraConfiguration.PREFIX + "." + prefix, StringConvention.RAW); + + Optional env = environment.getPropertySources().stream().filter(strings -> strings.getName().equals("env")).findFirst(); + env.ifPresent(envVars -> { + var cassandraPrefix = String.format("CASSANDRA_%s_", prefix.toUpperCase()); + // visit all the cassandra environment variables, like CASSANDRA_DEFAULT_BASIC_SESSION-NAME + ((MapPropertySource) envVars).asMap().entrySet().stream() + .filter(entry -> entry.getKey().startsWith(cassandraPrefix)) + // convert to property format datastax needs: + // CASSANDRA_DEFAULT_BASIC_SESSION-NAME -> cassandra.default.basic.session-name + // and put in configProperties, overriding existing properties (env overrides app config) + .forEach(e -> configProperties.put(e.getKey().replace(cassandraPrefix, "") + .toLowerCase().replaceAll("_", "."), e.getValue())); + }); + + // finally, the Datastax java driver is intolerant of Micronaut indexed properties, + // so translate them from Micronaut array index notation (i.e. foo[0]=bar) + // to Datastax driver decimal notation (i.e. foo.0=bar) + // e.g. cassandra.default.basic.contact-points[0] -> cassandra.default.basic.contact-points.1 + return configProperties.entrySet().stream().collect( + Collectors.toMap(e -> e.getKey().replaceAll("\\[(\\d+)]", ".$1"), + Map.Entry::getValue)); + } + /** * Creates the {@link CqlSession} bean for the given configuration. * diff --git a/cassandra/src/test/groovy/io/micronaut/cassandra/metrics/CassandraMetricsSpec.groovy b/cassandra/src/test/groovy/io/micronaut/cassandra/metrics/CassandraMetricsSpec.groovy index 41f10102..173a242f 100755 --- a/cassandra/src/test/groovy/io/micronaut/cassandra/metrics/CassandraMetricsSpec.groovy +++ b/cassandra/src/test/groovy/io/micronaut/cassandra/metrics/CassandraMetricsSpec.groovy @@ -1,15 +1,17 @@ package io.micronaut.cassandra.metrics import com.datastax.oss.driver.api.core.CqlSession +import io.micrometer.core.instrument.Counter import io.micrometer.core.instrument.Meter import io.micrometer.core.instrument.MeterRegistry +import io.micrometer.core.instrument.Timer import io.micronaut.context.ApplicationContext -import io.micronaut.context.DefaultApplicationContext -import io.micronaut.context.env.MapPropertySource import io.micronaut.inject.qualifiers.Qualifiers import org.testcontainers.containers.CassandraContainer import org.testcontainers.utility.DockerImageName +import spock.lang.Issue import spock.lang.Specification +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables class CassandraMetricsSpec extends Specification { @@ -55,4 +57,48 @@ class CassandraMetricsSpec extends Specification { cassandra.stop() applicationContext.close() } + + @Issue("https://github.com/micronaut-projects/micronaut-cassandra/issues/240") + void "test metrics with overriding env vars"() { + given: + // override: cassandra.default.basic.session-name=defaultSession + def env = new EnvironmentVariables("CASSANDRA_DEFAULT_BASIC_SESSION-NAME", "envSession") + env.setup() + + CassandraContainer cassandra = new CassandraContainer(DockerImageName.parse('cassandra:latest')) + cassandra.start() + + ApplicationContext applicationContext = ApplicationContext.run( + 'micronaut.metrics.enabled': true, + 'cassandra.default.basic.contact-points': ["localhost:$cassandra.firstMappedPort"], + 'cassandra.default.basic.session-name': 'defaultSession', + 'cassandra.default.advanced.metrics.factory.class': 'MicrometerMetricsFactory', + 'cassandra.default.advanced.metrics.session.enabled': ['connected-nodes', 'cql-requests'], + 'cassandra.default.basic.load-balancing-policy.local-datacenter': 'datacenter1', + 'test' + ) + + when: + CqlSession defaultCluster = applicationContext.getBean(CqlSession) + MeterRegistry meterRegistry = applicationContext.getBean(MeterRegistry) + + then: + defaultCluster + meterRegistry + + when: + Timer cqlRequests = meterRegistry.timer("envSession.cql-requests") + Counter bytesSent = meterRegistry.counter("envSession.bytes-sent") + Counter bytesReceived = meterRegistry.counter("envSession.bytes-received") + + then: + cqlRequests + bytesSent + bytesReceived + + cleanup: + env.teardown() + cassandra.stop() + applicationContext.close() + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3469dbdd..fbcac4d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ managed-datastax-cassandra-driver = "4.17.0" groovy = "4.0.14" spock = "2.3-groovy-4.0" testcontainers = "1.19.1" +system-stubs = "1.2.0" [libraries] # Core @@ -22,6 +23,7 @@ managed-datastax-cassandra-driver-core = { module = "com.datastax.oss:java-drive managed-datastax-cassandra-driver-mapper-processor = { module = "com.datastax.oss:java-driver-mapper-processor", version.ref = "managed-datastax-cassandra-driver" } managed-datastax-cassandra-driver-metrics-micrometer = { module = "com.datastax.oss:java-driver-metrics-micrometer", version.ref = "managed-datastax-cassandra-driver" } +system-stubs = { module = "uk.org.webcompere:system-stubs-core", version.ref = "system-stubs" } testcontainers-cassandra = { module = "org.testcontainers:cassandra", version.ref = "testcontainers" } testcontainers-spock = { module = "org.testcontainers:spock", version.ref = "testcontainers" } From f5597a6fb72a9891e14119d88826f67970107950 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Fri, 27 Oct 2023 15:27:54 -0500 Subject: [PATCH 2/9] fix code smells --- .../java/io/micronaut/cassandra/CassandraSessionFactory.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java b/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java index 7a74d423..985bb6b8 100644 --- a/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java +++ b/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java @@ -83,9 +83,6 @@ public CqlSessionBuilder session(CassandraConfiguration configuration, Environme /* Cassandra's Datastax java driver is rather intolerant of Micronaut configuration properties, so they need to be dealt with specially to pass to the driver. - - TODO: document this and add information to user documentation - TODO: add support for */ private Map dataStaxProperties(String prefix, Environment environment) { final Map configProperties = this.resolver @@ -101,7 +98,7 @@ private Map dataStaxProperties(String prefix, Environment enviro // CASSANDRA_DEFAULT_BASIC_SESSION-NAME -> cassandra.default.basic.session-name // and put in configProperties, overriding existing properties (env overrides app config) .forEach(e -> configProperties.put(e.getKey().replace(cassandraPrefix, "") - .toLowerCase().replaceAll("_", "."), e.getValue())); + .toLowerCase().replace("_", "."), e.getValue())); }); // finally, the Datastax java driver is intolerant of Micronaut indexed properties, From 36c8656391ff083aa6751d54ffd1d6ca7b2ce5f4 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Sat, 28 Oct 2023 01:21:17 -0500 Subject: [PATCH 3/9] simplify code --- .../cassandra/CassandraSessionFactory.java | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java b/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java index 985bb6b8..944e460a 100644 --- a/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java +++ b/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java @@ -23,7 +23,6 @@ import io.micronaut.context.annotation.Factory; import io.micronaut.context.env.Environment; import io.micronaut.context.env.MapPropertySource; -import io.micronaut.context.env.PropertySource; import io.micronaut.core.naming.conventions.StringConvention; import io.micronaut.core.value.PropertyResolver; import jakarta.annotation.PreDestroy; @@ -33,7 +32,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; /** @@ -88,26 +86,27 @@ private Map dataStaxProperties(String prefix, Environment enviro final Map configProperties = this.resolver .getProperties(CassandraConfiguration.PREFIX + "." + prefix, StringConvention.RAW); - Optional env = environment.getPropertySources().stream().filter(strings -> strings.getName().equals("env")).findFirst(); - env.ifPresent(envVars -> { - var cassandraPrefix = String.format("CASSANDRA_%s_", prefix.toUpperCase()); - // visit all the cassandra environment variables, like CASSANDRA_DEFAULT_BASIC_SESSION-NAME - ((MapPropertySource) envVars).asMap().entrySet().stream() - .filter(entry -> entry.getKey().startsWith(cassandraPrefix)) - // convert to property format datastax needs: - // CASSANDRA_DEFAULT_BASIC_SESSION-NAME -> cassandra.default.basic.session-name - // and put in configProperties, overriding existing properties (env overrides app config) - .forEach(e -> configProperties.put(e.getKey().replace(cassandraPrefix, "") - .toLowerCase().replace("_", "."), e.getValue())); - }); + environment.getPropertySources().stream() + .filter(strings -> strings.getName().equals("env")) + .findFirst() + .ifPresent(envVars -> { + // visit all the cassandra environment variables, e.g. CASSANDRA_DEFAULT_BASIC_SESSION-NAME + var cassandraPrefix = String.format("CASSANDRA_%s_", prefix.toUpperCase()); + ((MapPropertySource) envVars).asMap().entrySet().stream() + .filter(entry -> entry.getKey().startsWith(cassandraPrefix)) + // convert to property format datastax needs: + // CASSANDRA_DEFAULT_BASIC_SESSION-NAME -> cassandra.default.basic.session-name + // and put in configProperties, overriding existing properties (env overrides app config) + .forEach(e -> configProperties.put(e.getKey().replace(cassandraPrefix, "") + .toLowerCase().replace("_", "."), e.getValue())); + }); // finally, the Datastax java driver is intolerant of Micronaut indexed properties, // so translate them from Micronaut array index notation (i.e. foo[0]=bar) // to Datastax driver decimal notation (i.e. foo.0=bar) // e.g. cassandra.default.basic.contact-points[0] -> cassandra.default.basic.contact-points.1 - return configProperties.entrySet().stream().collect( - Collectors.toMap(e -> e.getKey().replaceAll("\\[(\\d+)]", ".$1"), - Map.Entry::getValue)); + return configProperties.entrySet().stream().collect(Collectors.toMap(e -> + e.getKey().replaceAll("\\[(\\d+)]", ".$1"), Map.Entry::getValue)); } /** From c3704eb0f9f5269ca632887ab4c98003a66f0160 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Mon, 30 Oct 2023 11:53:38 -0500 Subject: [PATCH 4/9] Add note about using environment variables to override application configuration for Datastax driver. --- src/main/docs/guide/additional.adoc | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/docs/guide/additional.adoc b/src/main/docs/guide/additional.adoc index 8037ac82..79f2c089 100644 --- a/src/main/docs/guide/additional.adoc +++ b/src/main/docs/guide/additional.adoc @@ -14,8 +14,8 @@ datastax-java-driver { } } ---- -.micronaut bean configuration - application.yml -[source,yaml] +.micronaut bean configuration - application configuration +[configuration] ---- cassandra: default: @@ -26,3 +26,8 @@ cassandra: load-balancing-policy: local-datacenter: datacenter1 ---- + +Micronaut Framework is very flexible with how application configuration is specified. Configuration properties can be overridden with environment variables. However, the Cassandra Datastax driver is not tolerant and fails fast when it encounters what it considers malformed/illegal configuration properties. Micronaut Cassandra handles this suatomatically for most cases, but if you want to use environment variables to override Cassandra application configuration the following rules apply: + +1. Must be uppercase camelcase, e.g. MY_UPPER_CAMEL_CASE_ENV +2. Where https://docs.datastax.com/en/developer/java-driver/latest/manual/core/configuration/reference/[DataStax driver configuration] specifies hyphenated configuration keys, the hyphens must be preserved, e.g. `cassandra.default.basic.session-name` is overridden with `CASSANDRA_DEFAULT_BASIC_SESSION-NAME`. From c8634d90fbe036ac89ef5b9066e375d004edc392 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Mon, 30 Oct 2023 11:54:54 -0500 Subject: [PATCH 5/9] spelling correction --- src/main/docs/guide/additional.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docs/guide/additional.adoc b/src/main/docs/guide/additional.adoc index 79f2c089..2251b8d8 100644 --- a/src/main/docs/guide/additional.adoc +++ b/src/main/docs/guide/additional.adoc @@ -27,7 +27,7 @@ cassandra: local-datacenter: datacenter1 ---- -Micronaut Framework is very flexible with how application configuration is specified. Configuration properties can be overridden with environment variables. However, the Cassandra Datastax driver is not tolerant and fails fast when it encounters what it considers malformed/illegal configuration properties. Micronaut Cassandra handles this suatomatically for most cases, but if you want to use environment variables to override Cassandra application configuration the following rules apply: +Micronaut Framework is very flexible with how application configuration is specified. Configuration properties can be overridden with environment variables. However, the Cassandra Datastax driver is not tolerant and fails fast when it encounters what it considers malformed/illegal configuration properties. Micronaut Cassandra handles this automatically for most cases, but if you want to use environment variables to override Cassandra application configuration the following rules apply: 1. Must be uppercase camelcase, e.g. MY_UPPER_CAMEL_CASE_ENV 2. Where https://docs.datastax.com/en/developer/java-driver/latest/manual/core/configuration/reference/[DataStax driver configuration] specifies hyphenated configuration keys, the hyphens must be preserved, e.g. `cassandra.default.basic.session-name` is overridden with `CASSANDRA_DEFAULT_BASIC_SESSION-NAME`. From 860e794c4cbf132dcc31d689b8cd965f7c592825 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Mon, 30 Oct 2023 12:07:48 -0500 Subject: [PATCH 6/9] fix comment --- .../java/io/micronaut/cassandra/CassandraSessionFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java b/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java index 944e460a..597a8c85 100644 --- a/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java +++ b/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java @@ -104,7 +104,7 @@ private Map dataStaxProperties(String prefix, Environment enviro // finally, the Datastax java driver is intolerant of Micronaut indexed properties, // so translate them from Micronaut array index notation (i.e. foo[0]=bar) // to Datastax driver decimal notation (i.e. foo.0=bar) - // e.g. cassandra.default.basic.contact-points[0] -> cassandra.default.basic.contact-points.1 + // e.g. cassandra.default.basic.contact-points[0] -> cassandra.default.basic.contact-points.0 return configProperties.entrySet().stream().collect(Collectors.toMap(e -> e.getKey().replaceAll("\\[(\\d+)]", ".$1"), Map.Entry::getValue)); } From 7edf5b603ef1103cb0fdbf954304c2d423cf9bfe Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Tue, 31 Oct 2023 12:09:53 -0500 Subject: [PATCH 7/9] fix/improve test --- cassandra/build.gradle | 3 +- .../metrics/CassandraMetricsSpec.groovy | 40 ++++++++----------- .../resources/application-envprops.properties | 8 ++++ .../test/resources/application-envyaml.yaml | 17 ++++++++ 4 files changed, 43 insertions(+), 25 deletions(-) create mode 100644 cassandra/src/test/resources/application-envprops.properties create mode 100644 cassandra/src/test/resources/application-envyaml.yaml diff --git a/cassandra/build.gradle b/cassandra/build.gradle index f245eba0..3cfc3234 100644 --- a/cassandra/build.gradle +++ b/cassandra/build.gradle @@ -16,9 +16,10 @@ dependencies { testImplementation(mn.micronaut.management) testImplementation(mnMicrometer.micronaut.micrometer.core) testImplementation(libs.system.stubs) + testRuntimeOnly(mn.snakeyaml) } tasks.withType(Test) { // this is needed by libs.system.stubs - jvmArgs = ["--add-opens", "java.base/java.util=ALL-UNNAMED", "--add-opens", "java.base/java.lang=ALL-UNNAMED"] + jvmArgs = ["--add-opens", "java.base/java.util=ALL-UNNAMED"] } diff --git a/cassandra/src/test/groovy/io/micronaut/cassandra/metrics/CassandraMetricsSpec.groovy b/cassandra/src/test/groovy/io/micronaut/cassandra/metrics/CassandraMetricsSpec.groovy index 173a242f..025f4d79 100755 --- a/cassandra/src/test/groovy/io/micronaut/cassandra/metrics/CassandraMetricsSpec.groovy +++ b/cassandra/src/test/groovy/io/micronaut/cassandra/metrics/CassandraMetricsSpec.groovy @@ -1,11 +1,10 @@ package io.micronaut.cassandra.metrics import com.datastax.oss.driver.api.core.CqlSession -import io.micrometer.core.instrument.Counter import io.micrometer.core.instrument.Meter import io.micrometer.core.instrument.MeterRegistry -import io.micrometer.core.instrument.Timer import io.micronaut.context.ApplicationContext +import io.micronaut.core.value.PropertyResolver import io.micronaut.inject.qualifiers.Qualifiers import org.testcontainers.containers.CassandraContainer import org.testcontainers.utility.DockerImageName @@ -59,46 +58,39 @@ class CassandraMetricsSpec extends Specification { } @Issue("https://github.com/micronaut-projects/micronaut-cassandra/issues/240") - void "test metrics with overriding env vars"() { + void "test metrics with overriding #configStyle with env vars"() { given: - // override: cassandra.default.basic.session-name=defaultSession - def env = new EnvironmentVariables("CASSANDRA_DEFAULT_BASIC_SESSION-NAME", "envSession") - env.setup() - CassandraContainer cassandra = new CassandraContainer(DockerImageName.parse('cassandra:latest')) cassandra.start() - ApplicationContext applicationContext = ApplicationContext.run( - 'micronaut.metrics.enabled': true, - 'cassandra.default.basic.contact-points': ["localhost:$cassandra.firstMappedPort"], - 'cassandra.default.basic.session-name': 'defaultSession', - 'cassandra.default.advanced.metrics.factory.class': 'MicrometerMetricsFactory', - 'cassandra.default.advanced.metrics.session.enabled': ['connected-nodes', 'cql-requests'], - 'cassandra.default.basic.load-balancing-policy.local-datacenter': 'datacenter1', - 'test' + // override: cassandra.default.basic.session-name=defaultSession + def env = new EnvironmentVariables( + "CASSANDRA_DEFAULT_BASIC_SESSION_NAME", "envSession", + "CASSANDRA_PORT", "${cassandra.firstMappedPort}" ) + env.setup() + + ApplicationContext applicationContext = ApplicationContext.run("env$configStyle") when: CqlSession defaultCluster = applicationContext.getBean(CqlSession) + PropertyResolver resolver = applicationContext.getBean(PropertyResolver) MeterRegistry meterRegistry = applicationContext.getBean(MeterRegistry) then: + resolver.getRequiredProperty("configuration", String) == configStyle defaultCluster meterRegistry - when: - Timer cqlRequests = meterRegistry.timer("envSession.cql-requests") - Counter bytesSent = meterRegistry.counter("envSession.bytes-sent") - Counter bytesReceived = meterRegistry.counter("envSession.bytes-received") - - then: - cqlRequests - bytesSent - bytesReceived + and: + meterRegistry.meters.id.name.findAll { it.contains("envSession") } ==~ ['envSession.connected-nodes', 'envSession.cql-requests'] cleanup: env.teardown() cassandra.stop() applicationContext.close() + + where: + configStyle << ['props', 'yaml'] } } diff --git a/cassandra/src/test/resources/application-envprops.properties b/cassandra/src/test/resources/application-envprops.properties new file mode 100644 index 00000000..b07c84f3 --- /dev/null +++ b/cassandra/src/test/resources/application-envprops.properties @@ -0,0 +1,8 @@ +configuration=props +micronaut.metrics.enabled=true +cassandra.default.basic.contact-points[0]=localhost:${cassandra.port} +cassandra.default.basic.session-name=defaultSession +cassandra.default.advanced.metrics.factory.class=MicrometerMetricsFactory +cassandra.default.advanced.metrics.session.enabled[0]=connected-nodes +cassandra.default.advanced.metrics.session.enabled[1]=cql-requests +cassandra.default.basic.load-balancing-policy.local-datacenter=datacenter1 diff --git a/cassandra/src/test/resources/application-envyaml.yaml b/cassandra/src/test/resources/application-envyaml.yaml new file mode 100644 index 00000000..cf364f2e --- /dev/null +++ b/cassandra/src/test/resources/application-envyaml.yaml @@ -0,0 +1,17 @@ +micronaut: + metrics: + enabled: true +configuration: yaml +cassandra: + default: + basic: + contact-points: ["localhost:${cassandra.port}"] + session-name: defaultSession + load-balancing-policy: + local-datacenter: datacenter1 + advanced: + metrics: + factory: + class: MicrometerMetricsFactory + session: + enabled: [connected-nodes, cql-requests] From 97edaa938652ffa6f27d20b7b073bef64909d31c Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Tue, 31 Oct 2023 12:12:35 -0500 Subject: [PATCH 8/9] improvement based on review feedback --- .../cassandra/CassandraSessionFactory.java | 49 +++++-------------- 1 file changed, 11 insertions(+), 38 deletions(-) diff --git a/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java b/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java index 597a8c85..74ca68fe 100644 --- a/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java +++ b/cassandra/src/main/java/io/micronaut/cassandra/CassandraSessionFactory.java @@ -21,15 +21,13 @@ import com.typesafe.config.ConfigFactory; import io.micronaut.context.annotation.EachBean; import io.micronaut.context.annotation.Factory; -import io.micronaut.context.env.Environment; -import io.micronaut.context.env.MapPropertySource; -import io.micronaut.core.naming.conventions.StringConvention; import io.micronaut.core.value.PropertyResolver; import jakarta.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -39,6 +37,7 @@ * * @author Nirav Assar * @author Michael Pollind + * @author Dean Wette * @since 1.0 */ @Factory @@ -61,16 +60,21 @@ public CassandraSessionFactory(PropertyResolver propertyResolver) { * Creates the {@link CqlSessionBuilder} bean for the given configuration. * * @param configuration The cassandra configuration bean - * @param environment The environment with possibly overriding environment variables * @return A {@link CqlSession} bean */ @EachBean(CassandraConfiguration.class) - public CqlSessionBuilder session(CassandraConfiguration configuration, Environment environment) { + public CqlSessionBuilder session(CassandraConfiguration configuration) { try { return CqlSession.builder().withConfigLoader(new DefaultDriverConfigLoader(() -> { ConfigFactory.invalidateCaches(); - var dataStaxProperties = dataStaxProperties(configuration.getName(), environment); - return ConfigFactory.parseMap(dataStaxProperties).withFallback(ConfigFactory.load().getConfig(DefaultDriverConfigLoader.DEFAULT_ROOT_PATH)); + String prefix = configuration.getName(); + Map properties = this.resolver.getProperties(CassandraConfiguration.PREFIX + "." + prefix); + // translate indexed properties for list values from Micronaut array index notation (i.e. foo[0]=bar) + // to Datastax driver decimal notation (i.e. foo.0=bar) + properties = properties.entrySet().stream() + .filter(e -> !(e.getValue() instanceof Collection)) + .collect((Collectors.toMap(e -> e.getKey().replaceAll("\\[(\\d+)]", ".$1"), Map.Entry::getValue))); + return ConfigFactory.parseMap(properties).withFallback(ConfigFactory.load().getConfig(DefaultDriverConfigLoader.DEFAULT_ROOT_PATH)); })); } catch (Exception e) { LOG.error(String.format("Failed to instantiate CQL session: %s", e.getMessage()), e); @@ -78,37 +82,6 @@ public CqlSessionBuilder session(CassandraConfiguration configuration, Environme } } - /* - Cassandra's Datastax java driver is rather intolerant of Micronaut configuration properties, - so they need to be dealt with specially to pass to the driver. - */ - private Map dataStaxProperties(String prefix, Environment environment) { - final Map configProperties = this.resolver - .getProperties(CassandraConfiguration.PREFIX + "." + prefix, StringConvention.RAW); - - environment.getPropertySources().stream() - .filter(strings -> strings.getName().equals("env")) - .findFirst() - .ifPresent(envVars -> { - // visit all the cassandra environment variables, e.g. CASSANDRA_DEFAULT_BASIC_SESSION-NAME - var cassandraPrefix = String.format("CASSANDRA_%s_", prefix.toUpperCase()); - ((MapPropertySource) envVars).asMap().entrySet().stream() - .filter(entry -> entry.getKey().startsWith(cassandraPrefix)) - // convert to property format datastax needs: - // CASSANDRA_DEFAULT_BASIC_SESSION-NAME -> cassandra.default.basic.session-name - // and put in configProperties, overriding existing properties (env overrides app config) - .forEach(e -> configProperties.put(e.getKey().replace(cassandraPrefix, "") - .toLowerCase().replace("_", "."), e.getValue())); - }); - - // finally, the Datastax java driver is intolerant of Micronaut indexed properties, - // so translate them from Micronaut array index notation (i.e. foo[0]=bar) - // to Datastax driver decimal notation (i.e. foo.0=bar) - // e.g. cassandra.default.basic.contact-points[0] -> cassandra.default.basic.contact-points.0 - return configProperties.entrySet().stream().collect(Collectors.toMap(e -> - e.getKey().replaceAll("\\[(\\d+)]", ".$1"), Map.Entry::getValue)); - } - /** * Creates the {@link CqlSession} bean for the given configuration. * From 40867ce3b66c822598a3b739d25de81f3e0db262 Mon Sep 17 00:00:00 2001 From: Dean Wette Date: Wed, 1 Nov 2023 08:59:42 -0500 Subject: [PATCH 9/9] revise doc --- src/main/docs/guide/additional.adoc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/docs/guide/additional.adoc b/src/main/docs/guide/additional.adoc index 2251b8d8..3626472e 100644 --- a/src/main/docs/guide/additional.adoc +++ b/src/main/docs/guide/additional.adoc @@ -27,7 +27,4 @@ cassandra: local-datacenter: datacenter1 ---- -Micronaut Framework is very flexible with how application configuration is specified. Configuration properties can be overridden with environment variables. However, the Cassandra Datastax driver is not tolerant and fails fast when it encounters what it considers malformed/illegal configuration properties. Micronaut Cassandra handles this automatically for most cases, but if you want to use environment variables to override Cassandra application configuration the following rules apply: - -1. Must be uppercase camelcase, e.g. MY_UPPER_CAMEL_CASE_ENV -2. Where https://docs.datastax.com/en/developer/java-driver/latest/manual/core/configuration/reference/[DataStax driver configuration] specifies hyphenated configuration keys, the hyphens must be preserved, e.g. `cassandra.default.basic.session-name` is overridden with `CASSANDRA_DEFAULT_BASIC_SESSION-NAME`. +Micronaut Framework is very flexible with how application configuration is specified. Configuration properties can be overridden with environment variables. However, the Cassandra Datastax driver is not tolerant and fails fast when it encounters what it considers malformed/illegal configuration properties. Micronaut Cassandra handles this automatically for most cases, but if you want to use environment variables to override https://docs.datastax.com/en/developer/java-driver/latest/manual/core/configuration/reference/[DataStax driver configuration] they must be uppercase camelcase, e.g. `CASSANDRA_DEFAULT_BASIC_SESSION_NAME`