From dc34ce840ba9e91d461b54a5378e613342e9a940 Mon Sep 17 00:00:00 2001 From: neon-hippo Date: Wed, 27 May 2026 08:12:27 -0500 Subject: [PATCH] feat(cloudspanner): add Google Cloud Spanner monitoring templates Co-Authored-By: Claude Sonnet 4.6 --- .../hertzbeat-collector-cloudspanner/pom.xml | 59 ++ .../CloudSpannerDriverLoader.java | 46 ++ .../CloudSpannerDriverLoaderTest.java | 38 + .../hertzbeat-collector-collector/pom.xml | 5 + .../SpannerJdbcTemplateIntegrationTest.java | 689 ++++++++++++++++++ hertzbeat-collector/pom.xml | 1 + .../resources/define/app-cloudspanner-pg.yml | 463 ++++++++++++ .../resources/define/app-cloudspanner.yml | 474 ++++++++++++ hertzbeat-warehouse/pom.xml | 4 + pom.xml | 15 + 10 files changed, 1794 insertions(+) create mode 100644 hertzbeat-collector/hertzbeat-collector-cloudspanner/pom.xml create mode 100644 hertzbeat-collector/hertzbeat-collector-cloudspanner/src/main/java/org/apache/hertzbeat/collector/collect/cloudspanner/CloudSpannerDriverLoader.java create mode 100644 hertzbeat-collector/hertzbeat-collector-cloudspanner/src/test/java/org/apache/hertzbeat/collector/collect/cloudspanner/CloudSpannerDriverLoaderTest.java create mode 100644 hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/spanner/SpannerJdbcTemplateIntegrationTest.java create mode 100644 hertzbeat-manager/src/main/resources/define/app-cloudspanner-pg.yml create mode 100644 hertzbeat-manager/src/main/resources/define/app-cloudspanner.yml diff --git a/hertzbeat-collector/hertzbeat-collector-cloudspanner/pom.xml b/hertzbeat-collector/hertzbeat-collector-cloudspanner/pom.xml new file mode 100644 index 00000000000..db343dfcf1c --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-cloudspanner/pom.xml @@ -0,0 +1,59 @@ + + + + 4.0.0 + + org.apache.hertzbeat + hertzbeat-collector + 2.0-SNAPSHOT + + + hertzbeat-collector-cloudspanner + ${project.artifactId} + + + ${java.version} + ${java.version} + UTF-8 + + + + + org.apache.hertzbeat + hertzbeat-collector-common + provided + + + org.springframework.boot + spring-boot-starter + provided + + + + com.google.cloud + google-cloud-spanner-jdbc + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/hertzbeat-collector/hertzbeat-collector-cloudspanner/src/main/java/org/apache/hertzbeat/collector/collect/cloudspanner/CloudSpannerDriverLoader.java b/hertzbeat-collector/hertzbeat-collector-cloudspanner/src/main/java/org/apache/hertzbeat/collector/collect/cloudspanner/CloudSpannerDriverLoader.java new file mode 100644 index 00000000000..5413521cf27 --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-cloudspanner/src/main/java/org/apache/hertzbeat/collector/collect/cloudspanner/CloudSpannerDriverLoader.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 org.apache.hertzbeat.collector.collect.cloudspanner; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; + +/** + * Force-loads the Cloud Spanner JDBC driver at startup to prevent SPI + * concurrent deadlock when multiple threads first access the driver + * simultaneously. Mirrors the pattern in JdbcSpiLoader for the drivers + * bundled in hertzbeat-collector-basic. + */ +@Service +@Slf4j +@Order(value = Ordered.HIGHEST_PRECEDENCE) +public class CloudSpannerDriverLoader implements CommandLineRunner { + + @Override + public void run(String... args) { + log.info("loading Cloud Spanner JDBC driver"); + try { + Class.forName("com.google.cloud.spanner.jdbc.JdbcDriver"); + } catch (Exception e) { + log.error("failed to load Cloud Spanner JDBC driver: {}", e.getMessage()); + } + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-cloudspanner/src/test/java/org/apache/hertzbeat/collector/collect/cloudspanner/CloudSpannerDriverLoaderTest.java b/hertzbeat-collector/hertzbeat-collector-cloudspanner/src/test/java/org/apache/hertzbeat/collector/collect/cloudspanner/CloudSpannerDriverLoaderTest.java new file mode 100644 index 00000000000..7edc5d3a0dd --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-cloudspanner/src/test/java/org/apache/hertzbeat/collector/collect/cloudspanner/CloudSpannerDriverLoaderTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 org.apache.hertzbeat.collector.collect.cloudspanner; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.sql.DriverManager; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link CloudSpannerDriverLoader} + */ +class CloudSpannerDriverLoaderTest { + + @Test + void spannerDriverMustBeRegisteredAfterLoad() throws Exception { + new CloudSpannerDriverLoader().run(); + assertNotNull( + DriverManager.getDriver( + "jdbc:cloudspanner:/projects/p/instances/i/databases/d"), + "Cloud Spanner JDBC driver must be registered after CloudSpannerDriverLoader runs"); + } +} diff --git a/hertzbeat-collector/hertzbeat-collector-collector/pom.xml b/hertzbeat-collector/hertzbeat-collector-collector/pom.xml index 2e0a08d2166..960dddfa7cb 100644 --- a/hertzbeat-collector/hertzbeat-collector-collector/pom.xml +++ b/hertzbeat-collector/hertzbeat-collector-collector/pom.xml @@ -127,6 +127,11 @@ mssql-jdbc test + + org.apache.hertzbeat + hertzbeat-collector-cloudspanner + ${hertzbeat.version} + diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/spanner/SpannerJdbcTemplateIntegrationTest.java b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/spanner/SpannerJdbcTemplateIntegrationTest.java new file mode 100644 index 00000000000..9d01064709b --- /dev/null +++ b/hertzbeat-collector/hertzbeat-collector-collector/src/test/java/org/apache/hertzbeat/collector/collect/database/spanner/SpannerJdbcTemplateIntegrationTest.java @@ -0,0 +1,689 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 org.apache.hertzbeat.collector.collect.database.spanner; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.InstanceConfigId; +import com.google.cloud.spanner.InstanceId; +import com.google.cloud.spanner.InstanceInfo; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerOptions; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.hertzbeat.collector.collect.strategy.CollectStrategyFactory; +import org.apache.hertzbeat.collector.dispatch.CollectDataDispatch; +import org.apache.hertzbeat.collector.dispatch.MetricsCollect; +import org.apache.hertzbeat.collector.timer.WheelTimerTask; +import org.apache.hertzbeat.common.entity.job.Job; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.timer.Timeout; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicContainer; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.yaml.snakeyaml.Yaml; + +/** + * Integration test for app-cloudspanner.yml (Google Standard SQL dialect) and + * app-cloudspanner-pg.yml (PostgreSQL dialect) against the official Cloud Spanner + * emulator. A single emulator container hosts both databases; the GSS database + * is created via autoConfigEmulator and the PG database via the Spanner Java client. + * SPANNER_SYS statistics tables are not supported by the emulator and are therefore + * not covered here. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SpannerJdbcTemplateIntegrationTest { + + private static final String EMULATOR_IMAGE = + "gcr.io/cloud-spanner-emulator/emulator:1.5.53"; + private static final int GRPC_PORT = 9010; + + private static final String PROJECT = "test-project"; + private static final String INSTANCE = "test-instance"; + private static final String DATABASE = "test-db"; + private static final String PG_DATABASE = "pg-test-db"; + + // SPANNER_SYS statistics views are not available in the emulator + private static final java.util.Set EMULATOR_UNSUPPORTED_GROUPS = + java.util.Set.of("query_stats", "txn_stats", "lock_stats", "table_sizes"); + + private GenericContainer emulator; + private String jdbcUrl; + private String pgJdbcUrl; + private List templateMetrics; + private List pgTemplateMetrics; + + @BeforeAll + void setUp() throws Exception { + Assumptions.assumeTrue( + DockerClientFactory.instance().isDockerAvailable(), + "Docker/Podman is required for integration tests"); + + new CollectStrategyFactory().run(); + templateMetrics = loadTemplate().getMetrics(); + pgTemplateMetrics = loadPgTemplate().getMetrics(); + + emulator = new GenericContainer<>(DockerImageName.parse(EMULATOR_IMAGE)) + .withExposedPorts(GRPC_PORT) + .waitingFor(Wait.forListeningPort()); + emulator.start(); + + provisionEmulator(); + + jdbcUrl = String.format( + "jdbc:cloudspanner://%s:%d/projects/%s/instances/%s/databases/%s" + + ";usePlainText=true;autoConfigEmulator=true", + emulator.getHost(), + emulator.getMappedPort(GRPC_PORT), + PROJECT, INSTANCE, DATABASE); + createSchema(); + seedData(); + + pgJdbcUrl = String.format( + "jdbc:cloudspanner://%s:%d/projects/%s/instances/%s/databases/%s" + + ";usePlainText=true;autoConfigEmulator=true", + emulator.getHost(), + emulator.getMappedPort(GRPC_PORT), + PROJECT, INSTANCE, PG_DATABASE); + createPgSchema(); + seedPgData(); + } + + @AfterAll + void tearDown() { + if (emulator != null) { + emulator.stop(); + } + } + + // ── Contract: metric groups must exist ── + + @Test + @DisplayName("tables metric group must be defined") + void tablesShouldBeDefined() { + assertTrue(templateMetrics.stream() + .anyMatch(m -> "tables".equals(m.getName())), + "app-cloudspanner.yml must define a tables metric group"); + } + + @Test + @DisplayName("indexes metric group must be defined") + void indexesShouldBeDefined() { + assertTrue(templateMetrics.stream() + .anyMatch(m -> "indexes".equals(m.getName())), + "app-cloudspanner.yml must define an indexes metric group"); + } + + @Test + @DisplayName("database_options metric group must be defined") + void databaseOptionsShouldBeDefined() { + assertTrue(templateMetrics.stream() + .anyMatch(m -> "database_options".equals(m.getName())), + "app-cloudspanner.yml must define a database_options metric group"); + } + + // ── Field type contracts ── + + @Test + @DisplayName("tables fields must all be type string") + void tablesFieldTypesShouldBeStrings() { + Metrics m = findMetric("tables"); + for (Metrics.Field f : m.getFields()) { + assertEquals(1, f.getType(), + "tables." + f.getField() + " must be type 1 (string)"); + } + } + + @Test + @DisplayName("indexes fields must all be type string") + void indexesFieldTypesShouldBeStrings() { + Metrics m = findMetric("indexes"); + for (Metrics.Field f : m.getFields()) { + assertEquals(1, f.getType(), + "indexes." + f.getField() + " must be type 1 (string)"); + } + } + + @Test + @DisplayName("database_options fields must all be type string") + void databaseOptionsFieldTypesShouldBeStrings() { + Metrics m = findMetric("database_options"); + for (Metrics.Field f : m.getFields()) { + assertEquals(1, f.getType(), + "database_options." + f.getField() + " must be type 1 (string)"); + } + } + + // ── Every metric group must collect successfully ── + + @TestFactory + @DisplayName("all metric groups must collect successfully against the emulator") + Stream shouldCollectAllMetricGroups() { + return templateMetrics.stream() + .filter(tmpl -> !EMULATOR_UNSUPPORTED_GROUPS.contains(tmpl.getName())) + .map(tmpl -> DynamicContainer.dynamicContainer( + tmpl.getName(), + Stream.of( + DynamicTest.dynamicTest("collects without error", + () -> { + CollectRep.MetricsData data = + collect(materialize(tmpl)); + assertEquals( + CollectRep.Code.SUCCESS, + data.getCode(), + tmpl.getName() + " failed: " + + data.getMsg()); + assertEquals( + tmpl.getFields().size(), + data.getFieldsCount(), + tmpl.getName() + + " field count mismatch"); + })))); + } + + // ── tables-specific assertions ── + + @TestFactory + @DisplayName("tables metric group edge cases") + Stream tablesEdgeCases() { + return Stream.of( + DynamicTest.dynamicTest( + "returns at least one row", + () -> { + CollectRep.MetricsData data = + collect(materialize(findMetric("tables"))); + assertTrue(data.getValuesCount() > 0, + "tables must return at least one row"); + }), + DynamicTest.dynamicTest( + "test_table must appear", + () -> { + CollectRep.MetricsData data = + collect(materialize(findMetric("tables"))); + boolean found = false; + for (int i = 0; i < data.getValuesCount(); i++) { + if ("test_table".equalsIgnoreCase( + data.getValues().get(i).getColumns(0))) { + found = true; + break; + } + } + assertTrue(found, + "tables must include test_table"); + }), + DynamicTest.dynamicTest( + "spanner_state must be COMMITTED for all rows", + () -> { + CollectRep.MetricsData data = + collect(materialize(findMetric("tables"))); + for (int i = 0; i < data.getValuesCount(); i++) { + assertEquals("COMMITTED", + data.getValues().get(i).getColumns(2), + "spanner_state must be COMMITTED"); + } + })); + } + + // ── indexes-specific assertions ── + + @TestFactory + @DisplayName("indexes metric group edge cases") + Stream indexesEdgeCases() { + return Stream.of( + DynamicTest.dynamicTest( + "returns at least one row", + () -> { + CollectRep.MetricsData data = + collect(materialize(findMetric("indexes"))); + assertTrue(data.getValuesCount() > 0, + "indexes must return at least one row"); + }), + DynamicTest.dynamicTest( + "secondary index idx_test_name must appear", + () -> { + CollectRep.MetricsData data = + collect(materialize(findMetric("indexes"))); + boolean found = false; + for (int i = 0; i < data.getValuesCount(); i++) { + if ("idx_test_name".equalsIgnoreCase( + data.getValues().get(i).getColumns(1))) { + found = true; + break; + } + } + assertTrue(found, + "indexes must include idx_test_name"); + }), + DynamicTest.dynamicTest( + "is_unique must be Y or N for all rows", + () -> { + CollectRep.MetricsData data = + collect(materialize(findMetric("indexes"))); + for (int i = 0; i < data.getValuesCount(); i++) { + String v = data.getValues().get(i).getColumns(4); + assertTrue("Y".equals(v) || "N".equals(v), + "is_unique must be Y/N, got: " + v); + } + }), + DynamicTest.dynamicTest( + "is_null_filtered must be Y or N for all rows", + () -> { + CollectRep.MetricsData data = + collect(materialize(findMetric("indexes"))); + for (int i = 0; i < data.getValuesCount(); i++) { + String v = data.getValues().get(i).getColumns(5); + assertTrue("Y".equals(v) || "N".equals(v), + "is_null_filtered must be Y/N, got: " + v); + } + })); + } + + // ── database_options-specific assertions ── + + @Test + @DisplayName("database_options must return at least one row") + void databaseOptionsShouldReturnRows() throws Exception { + CollectRep.MetricsData data = + collect(materialize(findMetric("database_options"))); + assertTrue(data.getValuesCount() > 0, + "database_options must return at least one row"); + } + + @Test + @DisplayName("database_dialect option must be GOOGLE_STANDARD_SQL") + void databaseDialectShouldBeGoogleSql() throws Exception { + CollectRep.MetricsData data = + collect(materialize(findMetric("database_options"))); + boolean found = false; + for (int i = 0; i < data.getValuesCount(); i++) { + if ("database_dialect".equalsIgnoreCase( + data.getValues().get(i).getColumns(0))) { + assertEquals("GOOGLE_STANDARD_SQL", + data.getValues().get(i).getColumns(1), + "database_dialect must be GOOGLE_STANDARD_SQL"); + found = true; + break; + } + } + assertTrue(found, "database_options must include database_dialect"); + } + + private void createSchema() throws Exception { + try (Connection conn = DriverManager.getConnection(jdbcUrl); + Statement stmt = conn.createStatement()) { + stmt.execute(""" + CREATE TABLE test_table ( + id INT64 NOT NULL, + name STRING(100) NOT NULL, + value FLOAT64 + ) PRIMARY KEY (id)"""); + stmt.execute( + "CREATE INDEX idx_test_name ON test_table (name)"); + } + } + + private void seedData() throws Exception { + try (Connection conn = DriverManager.getConnection(jdbcUrl); + var ps = conn.prepareStatement( + "INSERT INTO test_table (id, name, value) VALUES (?, ?, ?)")) { + ps.setLong(1, 1); + ps.setString(2, "alpha"); + ps.setDouble(3, 1.0); + ps.executeUpdate(); + ps.setLong(1, 2); + ps.setString(2, "beta"); + ps.setDouble(3, 2.0); + ps.executeUpdate(); + ps.setLong(1, 3); + ps.setString(2, "gamma"); + ps.setDouble(3, 3.0); + ps.executeUpdate(); + } + } + + // ── Collection helpers ── + + private CollectRep.MetricsData collect(Metrics metric) { + Job job = Job.builder() + .monitorId(1L).tenantId(1L).app("cloudspanner") + .defaultInterval(600L) + .metadata(new HashMap<>(0)) + .labels(new HashMap<>(0)) + .annotations(new HashMap<>(0)) + .configmap(new ArrayList<>(0)) + .metrics(new ArrayList<>(List.of(metric))) + .build(); + WheelTimerTask timerTask = new WheelTimerTask(job, timeout -> { }); + var dispatch = new CapturingCollectDataDispatch(); + var collector = new MetricsCollect( + metric, new StubTimeout(timerTask), dispatch, + "collector-test", List.of()); + collector.run(); + assertNotNull(dispatch.metricsData, + metric.getName() + " should dispatch metrics data"); + return dispatch.metricsData; + } + + private Metrics materialize(Metrics templateMetric) { + Metrics m = JsonUtil.fromJson( + JsonUtil.toJson(templateMetric), Metrics.class); + JdbcProtocol jdbc = m.getJdbc(); + jdbc.setUrl(jdbcUrl); + jdbc.setTimeout("30000"); + jdbc.setReuseConnection("false"); + if (m.getAliasFields() == null || m.getAliasFields().isEmpty()) { + m.setAliasFields( + m.getFields().stream() + .map(Metrics.Field::getField) + .collect(Collectors.toList())); + } + return m; + } + + private Metrics findMetric(String name) { + return templateMetrics.stream() + .filter(m -> name.equals(m.getName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "Metric group not found: " + name)); + } + + private Job loadTemplate() throws Exception { + Path path = Path.of( + "..", "..", "hertzbeat-manager", "src", "main", + "resources", "define", "app-cloudspanner.yml") + .toAbsolutePath().normalize(); + try (Reader r = Files.newBufferedReader(path)) { + return new Yaml().loadAs(r, Job.class); + } + } + + // ── Emulator provisioning ── + + private void provisionEmulator() throws Exception { + SpannerOptions opts = SpannerOptions.newBuilder() + .setEmulatorHost(emulator.getHost() + ":" + + emulator.getMappedPort(GRPC_PORT)) + .setProjectId(PROJECT) + .build(); + try (Spanner spanner = opts.getService()) { + spanner.getInstanceAdminClient() + .createInstance(InstanceInfo + .newBuilder(InstanceId.of(PROJECT, INSTANCE)) + .setDisplayName(INSTANCE) + .setInstanceConfigId( + InstanceConfigId.of(PROJECT, "emulator-config")) + .setNodeCount(1) + .build()) + .get(); + var dbAdmin = spanner.getDatabaseAdminClient(); + dbAdmin.createDatabase(INSTANCE, DATABASE, List.of()).get(); + dbAdmin.createDatabase(INSTANCE, + "CREATE DATABASE \"" + PG_DATABASE + "\"", + Dialect.POSTGRESQL, List.of()).get(); + } + } + + private void createPgSchema() throws Exception { + try (Connection conn = DriverManager.getConnection(pgJdbcUrl); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE test_table (" + + "id bigint PRIMARY KEY," + + " name varchar(100) NOT NULL," + + " value float8" + + ")"); + stmt.execute("CREATE INDEX idx_test_name ON test_table (name)"); + } + } + + private void seedPgData() throws Exception { + try (Connection conn = DriverManager.getConnection(pgJdbcUrl); + var ps = conn.prepareStatement( + "INSERT INTO test_table (id, name, value)" + + " VALUES (?, ?, ?)")) { + ps.setLong(1, 1); + ps.setString(2, "alpha"); + ps.setDouble(3, 1.0); + ps.executeUpdate(); + ps.setLong(1, 2); + ps.setString(2, "beta"); + ps.setDouble(3, 2.0); + ps.executeUpdate(); + ps.setLong(1, 3); + ps.setString(2, "gamma"); + ps.setDouble(3, 3.0); + ps.executeUpdate(); + } + } + + private Job loadPgTemplate() throws Exception { + Path path = Path.of( + "..", "..", "hertzbeat-manager", "src", "main", + "resources", "define", "app-cloudspanner-pg.yml") + .toAbsolutePath().normalize(); + try (Reader r = Files.newBufferedReader(path)) { + return new Yaml().loadAs(r, Job.class); + } + } + + private Metrics materializePg(Metrics templateMetric) { + Metrics m = JsonUtil.fromJson( + JsonUtil.toJson(templateMetric), Metrics.class); + JdbcProtocol jdbc = m.getJdbc(); + jdbc.setUrl(pgJdbcUrl); + jdbc.setTimeout("30000"); + jdbc.setReuseConnection("false"); + if (m.getAliasFields() == null || m.getAliasFields().isEmpty()) { + m.setAliasFields( + m.getFields().stream() + .map(Metrics.Field::getField) + .collect(Collectors.toList())); + } + return m; + } + + private Metrics findPgMetric(String name) { + return pgTemplateMetrics.stream() + .filter(m -> name.equals(m.getName())) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "PG metric group not found: " + name)); + } + + // ── PG dialect contract tests ── + + @Test + @DisplayName("PG tables metric group must be defined") + void pgTablesShouldBeDefined() { + assertTrue(pgTemplateMetrics.stream() + .anyMatch(m -> "tables".equals(m.getName())), + "app-cloudspanner-pg.yml must define a tables metric group"); + } + + @Test + @DisplayName("PG indexes metric group must be defined") + void pgIndexesShouldBeDefined() { + assertTrue(pgTemplateMetrics.stream() + .anyMatch(m -> "indexes".equals(m.getName())), + "app-cloudspanner-pg.yml must define an indexes metric group"); + } + + @Test + @DisplayName("PG database_options metric group must be defined") + void pgDatabaseOptionsShouldBeDefined() { + assertTrue(pgTemplateMetrics.stream() + .anyMatch(m -> "database_options".equals(m.getName())), + "app-cloudspanner-pg.yml must define a database_options metric group"); + } + + // ── PG dialect collection tests ── + + @TestFactory + @DisplayName("all PG metric groups must collect successfully against the emulator") + Stream shouldCollectAllPgMetricGroups() { + return pgTemplateMetrics.stream() + .filter(tmpl -> !EMULATOR_UNSUPPORTED_GROUPS.contains(tmpl.getName())) + .map(tmpl -> DynamicContainer.dynamicContainer( + "pg-" + tmpl.getName(), + Stream.of(DynamicTest.dynamicTest( + "collects without error", + () -> { + CollectRep.MetricsData data = + collect(materializePg(tmpl)); + assertEquals( + CollectRep.Code.SUCCESS, + data.getCode(), + tmpl.getName() + " failed: " + + data.getMsg()); + assertEquals( + tmpl.getFields().size(), + data.getFieldsCount(), + tmpl.getName() + + " field count mismatch"); + })))); + } + + @Test + @DisplayName("PG database_dialect option must be POSTGRESQL") + void pgDatabaseDialectShouldBePostgresql() throws Exception { + CollectRep.MetricsData data = + collect(materializePg(findPgMetric("database_options"))); + boolean found = false; + for (int i = 0; i < data.getValuesCount(); i++) { + if ("database_dialect".equalsIgnoreCase( + data.getValues().get(i).getColumns(0))) { + assertEquals("POSTGRESQL", + data.getValues().get(i).getColumns(1), + "database_dialect must be POSTGRESQL"); + found = true; + break; + } + } + assertTrue(found, + "database_options must include database_dialect"); + } + + @Test + @DisplayName("PG test_table must appear in tables metric group") + void pgTestTableShouldAppear() throws Exception { + CollectRep.MetricsData data = + collect(materializePg(findPgMetric("tables"))); + assertTrue(data.getValuesCount() > 0, + "PG tables must return at least one row"); + boolean found = false; + for (int i = 0; i < data.getValuesCount(); i++) { + if ("test_table".equalsIgnoreCase( + data.getValues().get(i).getColumns(0))) { + found = true; + break; + } + } + assertTrue(found, "PG tables must include test_table"); + } + + @Test + @DisplayName("PG idx_test_name must appear in indexes metric group") + void pgIndexShouldAppear() throws Exception { + CollectRep.MetricsData data = + collect(materializePg(findPgMetric("indexes"))); + assertTrue(data.getValuesCount() > 0, + "PG indexes must return at least one row"); + boolean found = false; + for (int i = 0; i < data.getValuesCount(); i++) { + if ("idx_test_name".equalsIgnoreCase( + data.getValues().get(i).getColumns(1))) { + found = true; + break; + } + } + assertTrue(found, "PG indexes must include idx_test_name"); + } + + // ── Inner types ── + + private static final class CapturingCollectDataDispatch + implements CollectDataDispatch { + private CollectRep.MetricsData metricsData; + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, + CollectRep.MetricsData data) { + this.metricsData = data; + } + + @Override + public void dispatchCollectData(Timeout timeout, Metrics metrics, + List list) { + if (list != null && !list.isEmpty()) { + this.metricsData = list.getFirst(); + } + } + } + + private record StubTimeout(WheelTimerTask wheelTimerTask) implements Timeout { + @Override + public org.apache.hertzbeat.common.timer.Timer timer() { + return null; + } + + @Override + public org.apache.hertzbeat.common.timer.TimerTask task() { + return wheelTimerTask; + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean cancel() { + return false; + } + } +} diff --git a/hertzbeat-collector/pom.xml b/hertzbeat-collector/pom.xml index f1e1454623c..05a489e4636 100644 --- a/hertzbeat-collector/pom.xml +++ b/hertzbeat-collector/pom.xml @@ -43,6 +43,7 @@ hertzbeat-collector-nebulagraph hertzbeat-collector-rocketmq hertzbeat-collector-kafka + hertzbeat-collector-cloudspanner diff --git a/hertzbeat-manager/src/main/resources/define/app-cloudspanner-pg.yml b/hertzbeat-manager/src/main/resources/define/app-cloudspanner-pg.yml new file mode 100644 index 00000000000..8a0576620d6 --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-cloudspanner-pg.yml @@ -0,0 +1,463 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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 +# +# http://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. + +category: db +app: cloudspanner-pg +name: + zh-CN: Google Cloud Spanner (PostgreSQL) + en-US: Google Cloud Spanner (PostgreSQL) + ja-JP: Google Cloud Spanner (PostgreSQL) +help: + zh-CN: > + Hertzbeat 通过 JDBC 对使用 PostgreSQL 方言的 Google Cloud Spanner 数据库进行监控。 + 仅适用于创建时指定了 PostgreSQL 方言的数据库。 + 请分别填写项目 ID、实例 ID 和数据库名称,Hertzbeat 将自动构建连接字符串。 + 如需附加 JDBC 连接属性(例如服务账号凭据),请在"额外参数"字段中以英文分号开头填写: + ;credentials=/path/to/key.json + en-US: > + Hertzbeat monitors Google Cloud Spanner databases using the PostgreSQL dialect via JDBC. + Use this template only for databases created with the PostgreSQL dialect. + Provide the project ID, instance ID, and database name separately — Hertzbeat + builds the connection URL automatically. + To add extra JDBC connection properties (e.g. a service account key file), enter + them in the Extra Params field starting with a semicolon: + ;credentials=/path/to/key.json + ja-JP: > + Hertzbeat は JDBC を使用して PostgreSQL 方言の Google Cloud Spanner データベースを監視します。 + PostgreSQL 方言で作成されたデータベースにのみ使用してください。 + プロジェクト ID、インスタンス ID、データベース名を個別に入力してください。 + 接続 URL は Hertzbeat が自動的に構築します。 +helpLink: + zh-CN: https://cloud.google.com/spanner/docs/use-oss-jdbc + en-US: https://cloud.google.com/spanner/docs/use-oss-jdbc + ja-JP: https://cloud.google.com/spanner/docs/use-oss-jdbc + +params: + - field: host + name: + zh-CN: 实例 ID + en-US: Instance ID + ja-JP: インスタンス ID + type: host + required: true + placeholder: 'my-instance' + - field: project + name: + zh-CN: 项目 ID + en-US: Project ID + ja-JP: プロジェクト ID + type: text + required: true + placeholder: 'my-gcp-project' + - field: database + name: + zh-CN: 数据库名 + en-US: Database Name + ja-JP: データベース名 + type: text + required: true + placeholder: 'my-database' + - field: params + name: + zh-CN: 额外参数 + en-US: Extra Params + ja-JP: 追加パラメータ + type: text + required: false + placeholder: ';credentials=/path/to/key.json' + - field: timeout + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + type: number + range: '[400,200000]' + required: true + defaultValue: 6000 + - field: reuseConnection + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + type: boolean + required: false + defaultValue: true + +metrics: + - name: tables + i18n: + zh-CN: 数据表 + en-US: Tables + ja-JP: テーブル + priority: 0 + fields: + - field: table_name + type: 1 + label: true + i18n: + zh-CN: 表名 + en-US: Table Name + ja-JP: テーブル名 + - field: table_type + type: 1 + i18n: + zh-CN: 表类型 + en-US: Table Type + ja-JP: テーブルタイプ + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT table_name, table_type + FROM information_schema.tables + WHERE table_schema = 'public' + ORDER BY table_name + + - name: indexes + i18n: + zh-CN: 索引 + en-US: Indexes + ja-JP: インデックス + priority: 1 + fields: + - field: table_name + type: 1 + label: true + i18n: + zh-CN: 表名 + en-US: Table Name + ja-JP: テーブル名 + - field: index_name + type: 1 + label: true + i18n: + zh-CN: 索引名 + en-US: Index Name + ja-JP: インデックス名 + - field: index_type + type: 1 + i18n: + zh-CN: 索引类型 + en-US: Index Type + ja-JP: インデックスタイプ + - field: index_state + type: 1 + i18n: + zh-CN: 索引状态 + en-US: Index State + ja-JP: インデックス状態 + - field: is_unique + type: 1 + i18n: + zh-CN: 是否唯一 + en-US: Is Unique + ja-JP: ユニーク + - field: is_null_filtered + type: 1 + i18n: + zh-CN: 是否过滤 NULL + en-US: Is Null Filtered + ja-JP: NULL フィルタ + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT table_name, index_name, index_type, index_state, + CASE WHEN is_unique = 'YES' THEN 'Y' ELSE 'N' END AS is_unique, + CASE WHEN is_null_filtered = 'YES' THEN 'Y' ELSE 'N' END AS is_null_filtered + FROM information_schema.indexes + WHERE table_schema = 'public' + ORDER BY table_name, index_name + + - name: database_options + i18n: + zh-CN: 数据库配置 + en-US: Database Options + ja-JP: データベースオプション + priority: 2 + fields: + - field: option_name + type: 1 + label: true + i18n: + zh-CN: 配置项 + en-US: Option Name + ja-JP: オプション名 + - field: option_value + type: 1 + i18n: + zh-CN: 配置值 + en-US: Option Value + ja-JP: オプション値 + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT option_name, option_value + FROM information_schema.database_options + ORDER BY option_name + + - name: query_stats + i18n: + zh-CN: 查询统计(近1分钟) + en-US: Query Stats (last minute) + ja-JP: クエリ統計(直近1分) + priority: 3 + fields: + - field: interval_end + type: 1 + i18n: + zh-CN: 统计截止时间 + en-US: Interval End + ja-JP: 集計終了時刻 + - field: execution_count + type: 0 + i18n: + zh-CN: 执行次数 + en-US: Execution Count + ja-JP: 実行回数 + - field: avg_latency_seconds + type: 0 + unit: s + i18n: + zh-CN: 平均延迟(s) + en-US: Avg Latency(s) + ja-JP: 平均レイテンシ(s) + - field: avg_rows + type: 0 + i18n: + zh-CN: 平均返回行数 + en-US: Avg Rows + ja-JP: 平均返却行数 + - field: avg_rows_scanned + type: 0 + i18n: + zh-CN: 平均扫描行数 + en-US: Avg Rows Scanned + ja-JP: 平均スキャン行数 + - field: avg_cpu_seconds + type: 0 + unit: s + i18n: + zh-CN: 平均 CPU 时间(s) + en-US: Avg CPU(s) + ja-JP: 平均 CPU 時間(s) + - field: all_failed_execution_count + type: 0 + i18n: + zh-CN: 失败次数 + en-US: Failed Count + ja-JP: 失敗回数 + - field: timed_out_execution_count + type: 0 + i18n: + zh-CN: 超时次数 + en-US: Timed Out Count + ja-JP: タイムアウト回数 + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT interval_end::text AS interval_end, + execution_count, + avg_latency_seconds, + avg_rows, + avg_rows_scanned, + avg_cpu_seconds, + all_failed_execution_count, + timed_out_execution_count + FROM SPANNER_SYS.QUERY_STATS_TOTAL_MINUTE + ORDER BY interval_end DESC + LIMIT 1 + + - name: txn_stats + i18n: + zh-CN: 事务统计(近1分钟) + en-US: Transaction Stats (last minute) + ja-JP: トランザクション統計(直近1分) + priority: 4 + fields: + - field: interval_end + type: 1 + i18n: + zh-CN: 统计截止时间 + en-US: Interval End + ja-JP: 集計終了時刻 + - field: commit_attempt_count + type: 0 + i18n: + zh-CN: 提交尝试次数 + en-US: Commit Attempts + ja-JP: コミット試行回数 + - field: commit_abort_count + type: 0 + i18n: + zh-CN: 中止次数 + en-US: Commit Aborts + ja-JP: コミット中止回数 + - field: commit_retry_count + type: 0 + i18n: + zh-CN: 重试次数 + en-US: Commit Retries + ja-JP: コミット再試行回数 + - field: commit_failed_precondition_count + type: 0 + i18n: + zh-CN: 前置条件失败次数 + en-US: Failed Precondition Count + ja-JP: 前提条件失敗回数 + - field: avg_total_latency_seconds + type: 0 + unit: s + i18n: + zh-CN: 平均总延迟(s) + en-US: Avg Total Latency(s) + ja-JP: 平均総レイテンシ(s) + - field: avg_commit_latency_seconds + type: 0 + unit: s + i18n: + zh-CN: 平均提交延迟(s) + en-US: Avg Commit Latency(s) + ja-JP: 平均コミットレイテンシ(s) + - field: avg_participants + type: 0 + i18n: + zh-CN: 平均参与者数 + en-US: Avg Participants + ja-JP: 平均参加者数 + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT interval_end::text AS interval_end, + commit_attempt_count, + commit_abort_count, + commit_retry_count, + commit_failed_precondition_count, + avg_total_latency_seconds, + avg_commit_latency_seconds, + avg_participants + FROM SPANNER_SYS.TXN_STATS_TOTAL_MINUTE + ORDER BY interval_end DESC + LIMIT 1 + + - name: lock_stats + i18n: + zh-CN: 锁等待统计(近1分钟) + en-US: Lock Stats (last minute) + ja-JP: ロック待機統計(直近1分) + priority: 5 + fields: + - field: interval_end + type: 1 + i18n: + zh-CN: 统计截止时间 + en-US: Interval End + ja-JP: 集計終了時刻 + - field: total_lock_wait_seconds + type: 0 + unit: s + i18n: + zh-CN: 总锁等待时间(s) + en-US: Total Lock Wait(s) + ja-JP: 合計ロック待機時間(s) + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT interval_end::text AS interval_end, + total_lock_wait_seconds + FROM SPANNER_SYS.LOCK_STATS_TOTAL_MINUTE + ORDER BY interval_end DESC + LIMIT 1 + + - name: table_sizes + i18n: + zh-CN: 表空间统计(近1小时) + en-US: Table Sizes (last hour) + ja-JP: テーブルサイズ統計(直近1時間) + priority: 6 + fields: + - field: table_name + type: 1 + label: true + i18n: + zh-CN: 表名 + en-US: Table Name + ja-JP: テーブル名 + - field: used_bytes + type: 0 + unit: B + i18n: + zh-CN: 已用字节 + en-US: Used Bytes + ja-JP: 使用バイト数 + - field: used_ssd_bytes + type: 0 + unit: B + i18n: + zh-CN: SSD 已用字节 + en-US: Used SSD Bytes + ja-JP: SSD 使用バイト数 + - field: used_hdd_bytes + type: 0 + unit: B + i18n: + zh-CN: HDD 已用字节 + en-US: Used HDD Bytes + ja-JP: HDD 使用バイト数 + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT table_name, + used_bytes, + used_ssd_bytes, + used_hdd_bytes + FROM SPANNER_SYS.TABLE_SIZES_STATS_1HOUR + WHERE interval_end = ( + SELECT MAX(interval_end) FROM SPANNER_SYS.TABLE_SIZES_STATS_1HOUR + ) + ORDER BY table_name diff --git a/hertzbeat-manager/src/main/resources/define/app-cloudspanner.yml b/hertzbeat-manager/src/main/resources/define/app-cloudspanner.yml new file mode 100644 index 00000000000..d1283a8d49d --- /dev/null +++ b/hertzbeat-manager/src/main/resources/define/app-cloudspanner.yml @@ -0,0 +1,474 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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 +# +# http://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. + +category: db +app: cloudspanner +name: + zh-CN: Google Cloud Spanner + en-US: Google Cloud Spanner + ja-JP: Google Cloud Spanner +help: + zh-CN: > + Hertzbeat 通过 JDBC 对 Google Cloud Spanner(Google Standard SQL 方言)进行监控。 + 请分别填写项目 ID、实例 ID 和数据库名称,Hertzbeat 将自动构建连接字符串。 + 如需附加 JDBC 连接属性(例如服务账号凭据),请在"额外参数"字段中以英文分号开头填写, + 例如:;credentials=/path/to/key.json + en-US: > + Hertzbeat monitors Google Cloud Spanner (Google Standard SQL dialect) via JDBC. + Provide the project ID, instance ID, and database name separately — Hertzbeat + builds the connection URL automatically. + To add extra JDBC connection properties (e.g. a service account key file), enter + them in the Extra Params field starting with a semicolon: + ;credentials=/path/to/key.json + ja-JP: > + Hertzbeat は JDBC を使用して Google Cloud Spanner(Google Standard SQL 方言)を監視します。 + プロジェクト ID、インスタンス ID、データベース名を個別に入力してください。 + 接続 URL は Hertzbeat が自動的に構築します。 + 追加の JDBC 接続プロパティ(サービスアカウントキーなど)が必要な場合は、 + 「追加パラメータ」フィールドにセミコロン始まりで入力してください。 +helpLink: + zh-CN: https://cloud.google.com/spanner/docs/use-oss-jdbc + en-US: https://cloud.google.com/spanner/docs/use-oss-jdbc + ja-JP: https://cloud.google.com/spanner/docs/use-oss-jdbc + +params: + - field: host + name: + zh-CN: 实例 ID + en-US: Instance ID + ja-JP: インスタンス ID + type: host + required: true + placeholder: 'my-instance' + - field: project + name: + zh-CN: 项目 ID + en-US: Project ID + ja-JP: プロジェクト ID + type: text + required: true + placeholder: 'my-gcp-project' + - field: database + name: + zh-CN: 数据库名 + en-US: Database Name + ja-JP: データベース名 + type: text + required: true + placeholder: 'my-database' + - field: params + name: + zh-CN: 额外参数 + en-US: Extra Params + ja-JP: 追加パラメータ + type: text + required: false + placeholder: ';credentials=/path/to/key.json' + - field: timeout + name: + zh-CN: 查询超时时间(ms) + en-US: Query Timeout(ms) + ja-JP: クエリタイムアウト(ms) + type: number + range: '[400,200000]' + required: true + defaultValue: 6000 + - field: reuseConnection + name: + zh-CN: 复用连接 + en-US: Reuse Connection + ja-JP: 接続再利用 + type: boolean + required: false + defaultValue: true + +metrics: + - name: tables + i18n: + zh-CN: 数据表 + en-US: Tables + ja-JP: テーブル + priority: 0 + fields: + - field: table_name + type: 1 + label: true + i18n: + zh-CN: 表名 + en-US: Table Name + ja-JP: テーブル名 + - field: table_type + type: 1 + i18n: + zh-CN: 表类型 + en-US: Table Type + ja-JP: テーブルタイプ + - field: spanner_state + type: 1 + i18n: + zh-CN: Spanner 状态 + en-US: Spanner State + ja-JP: Spannerの状態 + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT TABLE_NAME AS table_name, + TABLE_TYPE AS table_type, + SPANNER_STATE AS spanner_state + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = '' + ORDER BY TABLE_NAME + + - name: indexes + i18n: + zh-CN: 索引 + en-US: Indexes + ja-JP: インデックス + priority: 1 + fields: + - field: table_name + type: 1 + label: true + i18n: + zh-CN: 表名 + en-US: Table Name + ja-JP: テーブル名 + - field: index_name + type: 1 + label: true + i18n: + zh-CN: 索引名 + en-US: Index Name + ja-JP: インデックス名 + - field: index_type + type: 1 + i18n: + zh-CN: 索引类型 + en-US: Index Type + ja-JP: インデックスタイプ + - field: index_state + type: 1 + i18n: + zh-CN: 索引状态 + en-US: Index State + ja-JP: インデックス状態 + - field: is_unique + type: 1 + i18n: + zh-CN: 是否唯一 + en-US: Is Unique + ja-JP: ユニーク + - field: is_null_filtered + type: 1 + i18n: + zh-CN: 是否过滤 NULL + en-US: Is Null Filtered + ja-JP: NULL フィルタ + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT TABLE_NAME AS table_name, + INDEX_NAME AS index_name, + INDEX_TYPE AS index_type, + INDEX_STATE AS index_state, + CASE WHEN IS_UNIQUE THEN 'Y' ELSE 'N' END AS is_unique, + CASE WHEN IS_NULL_FILTERED THEN 'Y' ELSE 'N' END AS is_null_filtered + FROM INFORMATION_SCHEMA.INDEXES + WHERE TABLE_SCHEMA = '' + ORDER BY TABLE_NAME, INDEX_NAME + + - name: database_options + i18n: + zh-CN: 数据库配置 + en-US: Database Options + ja-JP: データベースオプション + priority: 2 + fields: + - field: option_name + type: 1 + label: true + i18n: + zh-CN: 配置项 + en-US: Option Name + ja-JP: オプション名 + - field: option_value + type: 1 + i18n: + zh-CN: 配置值 + en-US: Option Value + ja-JP: オプション値 + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT OPTION_NAME AS option_name, + OPTION_VALUE AS option_value + FROM INFORMATION_SCHEMA.DATABASE_OPTIONS + ORDER BY OPTION_NAME + + - name: query_stats + i18n: + zh-CN: 查询统计(近1分钟) + en-US: Query Stats (last minute) + ja-JP: クエリ統計(直近1分) + priority: 3 + fields: + - field: interval_end + type: 1 + i18n: + zh-CN: 统计截止时间 + en-US: Interval End + ja-JP: 集計終了時刻 + - field: execution_count + type: 0 + i18n: + zh-CN: 执行次数 + en-US: Execution Count + ja-JP: 実行回数 + - field: avg_latency_seconds + type: 0 + unit: s + i18n: + zh-CN: 平均延迟(s) + en-US: Avg Latency(s) + ja-JP: 平均レイテンシ(s) + - field: avg_rows + type: 0 + i18n: + zh-CN: 平均返回行数 + en-US: Avg Rows + ja-JP: 平均返却行数 + - field: avg_rows_scanned + type: 0 + i18n: + zh-CN: 平均扫描行数 + en-US: Avg Rows Scanned + ja-JP: 平均スキャン行数 + - field: avg_cpu_seconds + type: 0 + unit: s + i18n: + zh-CN: 平均 CPU 时间(s) + en-US: Avg CPU(s) + ja-JP: 平均 CPU 時間(s) + - field: all_failed_execution_count + type: 0 + i18n: + zh-CN: 失败次数 + en-US: Failed Count + ja-JP: 失敗回数 + - field: timed_out_execution_count + type: 0 + i18n: + zh-CN: 超时次数 + en-US: Timed Out Count + ja-JP: タイムアウト回数 + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT CAST(INTERVAL_END AS STRING) AS interval_end, + EXECUTION_COUNT AS execution_count, + AVG_LATENCY_SECONDS AS avg_latency_seconds, + AVG_ROWS AS avg_rows, + AVG_ROWS_SCANNED AS avg_rows_scanned, + AVG_CPU_SECONDS AS avg_cpu_seconds, + ALL_FAILED_EXECUTION_COUNT AS all_failed_execution_count, + TIMED_OUT_EXECUTION_COUNT AS timed_out_execution_count + FROM SPANNER_SYS.QUERY_STATS_TOTAL_MINUTE + ORDER BY INTERVAL_END DESC + LIMIT 1 + + - name: txn_stats + i18n: + zh-CN: 事务统计(近1分钟) + en-US: Transaction Stats (last minute) + ja-JP: トランザクション統計(直近1分) + priority: 4 + fields: + - field: interval_end + type: 1 + i18n: + zh-CN: 统计截止时间 + en-US: Interval End + ja-JP: 集計終了時刻 + - field: commit_attempt_count + type: 0 + i18n: + zh-CN: 提交尝试次数 + en-US: Commit Attempts + ja-JP: コミット試行回数 + - field: commit_abort_count + type: 0 + i18n: + zh-CN: 中止次数 + en-US: Commit Aborts + ja-JP: コミット中止回数 + - field: commit_retry_count + type: 0 + i18n: + zh-CN: 重试次数 + en-US: Commit Retries + ja-JP: コミット再試行回数 + - field: commit_failed_precondition_count + type: 0 + i18n: + zh-CN: 前置条件失败次数 + en-US: Failed Precondition Count + ja-JP: 前提条件失敗回数 + - field: avg_total_latency_seconds + type: 0 + unit: s + i18n: + zh-CN: 平均总延迟(s) + en-US: Avg Total Latency(s) + ja-JP: 平均総レイテンシ(s) + - field: avg_commit_latency_seconds + type: 0 + unit: s + i18n: + zh-CN: 平均提交延迟(s) + en-US: Avg Commit Latency(s) + ja-JP: 平均コミットレイテンシ(s) + - field: avg_participants + type: 0 + i18n: + zh-CN: 平均参与者数 + en-US: Avg Participants + ja-JP: 平均参加者数 + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT CAST(INTERVAL_END AS STRING) AS interval_end, + COMMIT_ATTEMPT_COUNT AS commit_attempt_count, + COMMIT_ABORT_COUNT AS commit_abort_count, + COMMIT_RETRY_COUNT AS commit_retry_count, + COMMIT_FAILED_PRECONDITION_COUNT AS commit_failed_precondition_count, + AVG_TOTAL_LATENCY_SECONDS AS avg_total_latency_seconds, + AVG_COMMIT_LATENCY_SECONDS AS avg_commit_latency_seconds, + AVG_PARTICIPANTS AS avg_participants + FROM SPANNER_SYS.TXN_STATS_TOTAL_MINUTE + ORDER BY INTERVAL_END DESC + LIMIT 1 + + - name: lock_stats + i18n: + zh-CN: 锁等待统计(近1分钟) + en-US: Lock Stats (last minute) + ja-JP: ロック待機統計(直近1分) + priority: 5 + fields: + - field: interval_end + type: 1 + i18n: + zh-CN: 统计截止时间 + en-US: Interval End + ja-JP: 集計終了時刻 + - field: total_lock_wait_seconds + type: 0 + unit: s + i18n: + zh-CN: 总锁等待时间(s) + en-US: Total Lock Wait(s) + ja-JP: 合計ロック待機時間(s) + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT CAST(INTERVAL_END AS STRING) AS interval_end, + TOTAL_LOCK_WAIT_SECONDS AS total_lock_wait_seconds + FROM SPANNER_SYS.LOCK_STATS_TOTAL_MINUTE + ORDER BY INTERVAL_END DESC + LIMIT 1 + + - name: table_sizes + i18n: + zh-CN: 表空间统计(近1小时) + en-US: Table Sizes (last hour) + ja-JP: テーブルサイズ統計(直近1時間) + priority: 6 + fields: + - field: table_name + type: 1 + label: true + i18n: + zh-CN: 表名 + en-US: Table Name + ja-JP: テーブル名 + - field: used_bytes + type: 0 + unit: B + i18n: + zh-CN: 已用字节 + en-US: Used Bytes + ja-JP: 使用バイト数 + - field: used_ssd_bytes + type: 0 + unit: B + i18n: + zh-CN: SSD 已用字节 + en-US: Used SSD Bytes + ja-JP: SSD 使用バイト数 + - field: used_hdd_bytes + type: 0 + unit: B + i18n: + zh-CN: HDD 已用字节 + en-US: Used HDD Bytes + ja-JP: HDD 使用バイト数 + protocol: jdbc + jdbc: + url: 'jdbc:cloudspanner:/projects/^_^project^_^/instances/^_^host^_^/databases/^_^database^_^^_^params^_^' + platform: cloudspanner + timeout: ^_^timeout^_^ + reuseConnection: ^_^reuseConnection^_^ + queryType: multiRow + sql: >- + SELECT TABLE_NAME AS table_name, + USED_BYTES AS used_bytes, + USED_SSD_BYTES AS used_ssd_bytes, + USED_HDD_BYTES AS used_hdd_bytes + FROM SPANNER_SYS.TABLE_SIZES_STATS_1HOUR + WHERE INTERVAL_END = ( + SELECT MAX(INTERVAL_END) FROM SPANNER_SYS.TABLE_SIZES_STATS_1HOUR + ) + ORDER BY TABLE_NAME diff --git a/hertzbeat-warehouse/pom.xml b/hertzbeat-warehouse/pom.xml index e65b30d5367..666a6cf8c90 100644 --- a/hertzbeat-warehouse/pom.xml +++ b/hertzbeat-warehouse/pom.xml @@ -117,6 +117,10 @@ com.google.protobuf protobuf-java + + io.grpc + grpc-netty-shaded + diff --git a/pom.xml b/pom.xml index c4d5ec7e95f..a19451886a3 100644 --- a/pom.xml +++ b/pom.xml @@ -136,6 +136,7 @@ 8.4.0 1.4.1 12.10.2.jre11 + 2.34.0 21.5.0.0 4.6.1 0.4.6 @@ -403,6 +404,20 @@ mssql-jdbc ${mssql-jdbc.version} + + + com.google.cloud + google-cloud-spanner-jdbc + ${google-cloud-spanner-jdbc.version} + + + + io.grpc + grpc-netty-shaded + 1.70.0 + com.oracle.database.jdbc