diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/DriverEnvironmentBuilder.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/DriverEnvironmentBuilder.java new file mode 100644 index 000000000000..2e8fff6cf9f6 --- /dev/null +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/DriverEnvironmentBuilder.java @@ -0,0 +1,178 @@ +/* + * Copyright 2026 Google LLC + * + * 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 com.google.cloud.bigquery.jdbc.telemetry.v1; + +import com.google.cloud.bigquery.jdbc.utils.BigQueryJdbcVersionUtility; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** Utility builder for constructing {@link DriverEnvironment} telemetry protos. */ +final class DriverEnvironmentBuilder { + private static final Logger logger = Logger.getLogger(DriverEnvironmentBuilder.class.getName()); + + static final String DRIVER_NAME = "google-bigquery-jdbc-driver"; + static final String CLIENT_LANGUAGE = "java"; + static final String DEFAULT_TELEMETRY_TAG_DIR = ".bigquery-jdbc"; + static final String DEFAULT_TELEMETRY_TAG_FILE = "telemetry-tag"; + static final String UNKNOWN = "unknown"; + static final String RESTRICTED = "restricted"; + + private DriverEnvironmentBuilder() {} + + static DriverEnvironment build() { + return build(null); + } + + static DriverEnvironment build(Path customTelemetryTagPath) { + return DriverEnvironment.newBuilder() + .setDriverName(DRIVER_NAME) + .setDriverVersion(BigQueryJdbcVersionUtility.getSanitizedDriverVersion()) + .setClientLanguage(CLIENT_LANGUAGE) + .setClientLanguageVersion(getMajorJavaVersion()) + .setOsType(detectOsType()) + .setOsVersion(getMajorOsVersion()) + .setTelemetryTag(getOrCreateTelemetryTag(customTelemetryTagPath)) + .build(); + } + + static String getMajorJavaVersion() { + try { + return getMajorJavaVersion(System.getProperty("java.version")); + } catch (SecurityException e) { + return RESTRICTED; + } + } + + static String getMajorJavaVersion(String versionProperty) { + if (versionProperty == null || versionProperty.trim().isEmpty()) { + return UNKNOWN; + } + String version = versionProperty.trim(); + if (version.startsWith("1.")) { + // Legacy Java version format (e.g. 1.8.0_292 -> 8) + String[] parts = version.split("\\."); + if (parts.length >= 2) { + return parts[1]; + } + } else { + // Modern Java version format (e.g. 11.0.12 -> 11 or 17.0.1 -> 17) + int firstDot = version.indexOf('.'); + if (firstDot > 0) { + return version.substring(0, firstDot); + } + } + return version; + } + + static DriverEnvironment.OsType detectOsType() { + try { + return detectOsType(System.getProperty("os.name")); + } catch (SecurityException e) { + return DriverEnvironment.OsType.OS_TYPE_UNSPECIFIED; + } + } + + static DriverEnvironment.OsType detectOsType(String osNameProperty) { + if (osNameProperty == null || osNameProperty.trim().isEmpty()) { + return DriverEnvironment.OsType.OS_TYPE_UNKNOWN; + } + String osName = osNameProperty.toLowerCase(); + if (osName.contains("mac") || osName.contains("darwin")) { + return DriverEnvironment.OsType.OS_TYPE_MACOS; + } else if (osName.contains("win")) { + return DriverEnvironment.OsType.OS_TYPE_WINDOWS; + } else if (osName.contains("nux") || osName.contains("nix")) { + return DriverEnvironment.OsType.OS_TYPE_LINUX; + } else if (osName.contains("solaris") || osName.contains("sunos")) { + return DriverEnvironment.OsType.OS_TYPE_SOLARIS; + } else if (osName.contains("freebsd")) { + return DriverEnvironment.OsType.OS_TYPE_FREEBSD; + } else if (osName.contains("openbsd")) { + return DriverEnvironment.OsType.OS_TYPE_OPENBSD; + } else if (osName.contains("netbsd")) { + return DriverEnvironment.OsType.OS_TYPE_NETBSD; + } else if (osName.contains("aix")) { + return DriverEnvironment.OsType.OS_TYPE_AIX; + } + return DriverEnvironment.OsType.OS_TYPE_UNKNOWN; + } + + static String getMajorOsVersion() { + try { + return getMajorOsVersion(System.getProperty("os.version")); + } catch (SecurityException e) { + return RESTRICTED; + } + } + + static String getMajorOsVersion(String osVersionProperty) { + if (osVersionProperty == null || osVersionProperty.trim().isEmpty()) { + return UNKNOWN; + } + String version = osVersionProperty.trim(); + int firstDot = version.indexOf('.'); + if (firstDot > 0) { + return version.substring(0, firstDot); + } + return version; + } + + static String getOrCreateTelemetryTag(Path customFilePath) { + try { + Path idFilePath = customFilePath; + if (idFilePath == null) { + String userHome = System.getProperty("user.home"); + if (userHome == null || userHome.trim().isEmpty()) { + return UUID.randomUUID().toString(); + } + idFilePath = Paths.get(userHome, DEFAULT_TELEMETRY_TAG_DIR, DEFAULT_TELEMETRY_TAG_FILE); + } + + if (Files.exists(idFilePath)) { + try { + String content = + new String(Files.readAllBytes(idFilePath), StandardCharsets.UTF_8).trim(); + // Validate existing content is a valid UUID + UUID.fromString(content); + return content; + } catch (Exception e) { + logger.log( + Level.WARNING, "Failed to read or parse telemetry tag from file, regenerating", e); + } + } + + String newId = UUID.randomUUID().toString(); + try { + if (idFilePath.getParent() != null) { + Files.createDirectories(idFilePath.getParent()); + } + Files.write(idFilePath, newId.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to persist telemetry tag to file", e); + } + return newId; + } catch (SecurityException e) { + return UUID.randomUUID().toString(); + } + } +} diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/utils/BigQueryJdbcVersionUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/utils/BigQueryJdbcVersionUtility.java index ece2ad4bd268..ebab86581f87 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/utils/BigQueryJdbcVersionUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/utils/BigQueryJdbcVersionUtility.java @@ -80,6 +80,14 @@ public static String getDriverVersion() { return DRIVER_VERSION; } + /** + * Returns a sanitized version of the driver version, containing only the major and minor + * components (e.g. "1.2" for "1.2.0-SNAPSHOT"). + */ + public static String getSanitizedDriverVersion() { + return DRIVER_MAJOR_VERSION + "." + DRIVER_MINOR_VERSION; + } + public static int getDriverMajorVersion() { return DRIVER_MAJOR_VERSION; } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/DriverEnvironmentBuilderTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/DriverEnvironmentBuilderTest.java new file mode 100644 index 000000000000..835db26e08fc --- /dev/null +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/DriverEnvironmentBuilderTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 2026 Google LLC + * + * 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 com.google.cloud.bigquery.jdbc.telemetry.v1; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class DriverEnvironmentBuilderTest { + + @Test + public void testBuildDriverEnvironment() { + DriverEnvironment env = DriverEnvironmentBuilder.build(); + assertNotNull(env); + assertEquals("google-bigquery-jdbc-driver", env.getDriverName()); + assertNotNull(env.getDriverVersion()); + assertEquals("java", env.getClientLanguage()); + assertNotNull(env.getClientLanguageVersion()); + assertNotNull(env.getOsType()); + assertNotNull(env.getOsVersion()); + assertNotNull(env.getTelemetryTag()); + } + + @Test + public void testBuildDriverEnvironmentCustomTagPath(@TempDir Path tempDir) { + Path tagFile = tempDir.resolve("telemetry-tag"); + DriverEnvironment env = DriverEnvironmentBuilder.build(tagFile); + assertNotNull(env); + assertEquals("google-bigquery-jdbc-driver", env.getDriverName()); + assertNotNull(env.getDriverVersion()); + assertEquals("java", env.getClientLanguage()); + assertNotNull(env.getClientLanguageVersion()); + assertNotNull(env.getOsType()); + assertNotNull(env.getOsVersion()); + assertEquals(env.getTelemetryTag(), DriverEnvironmentBuilder.getOrCreateTelemetryTag(tagFile)); + } + + @Test + public void testGetMajorJavaVersion() { + assertEquals("8", DriverEnvironmentBuilder.getMajorJavaVersion("1.8.0_292")); + assertEquals("11", DriverEnvironmentBuilder.getMajorJavaVersion("11.0.12")); + assertEquals("17", DriverEnvironmentBuilder.getMajorJavaVersion("17.0.1")); + assertEquals("21", DriverEnvironmentBuilder.getMajorJavaVersion("21")); + assertEquals("unknown", DriverEnvironmentBuilder.getMajorJavaVersion(null)); + assertEquals("unknown", DriverEnvironmentBuilder.getMajorJavaVersion(" ")); + } + + @Test + public void testDetectOsType() { + assertEquals( + DriverEnvironment.OsType.OS_TYPE_WINDOWS, + DriverEnvironmentBuilder.detectOsType("Windows 11")); + assertEquals( + DriverEnvironment.OsType.OS_TYPE_MACOS, DriverEnvironmentBuilder.detectOsType("Mac OS X")); + assertEquals( + DriverEnvironment.OsType.OS_TYPE_MACOS, DriverEnvironmentBuilder.detectOsType("Darwin")); + assertEquals( + DriverEnvironment.OsType.OS_TYPE_LINUX, DriverEnvironmentBuilder.detectOsType("Linux")); + assertEquals( + DriverEnvironment.OsType.OS_TYPE_SOLARIS, DriverEnvironmentBuilder.detectOsType("Solaris")); + assertEquals( + DriverEnvironment.OsType.OS_TYPE_FREEBSD, DriverEnvironmentBuilder.detectOsType("FreeBSD")); + assertEquals( + DriverEnvironment.OsType.OS_TYPE_OPENBSD, DriverEnvironmentBuilder.detectOsType("OpenBSD")); + assertEquals( + DriverEnvironment.OsType.OS_TYPE_NETBSD, DriverEnvironmentBuilder.detectOsType("NetBSD")); + assertEquals( + DriverEnvironment.OsType.OS_TYPE_AIX, DriverEnvironmentBuilder.detectOsType("AIX")); + assertEquals( + DriverEnvironment.OsType.OS_TYPE_UNKNOWN, + DriverEnvironmentBuilder.detectOsType("UnknownOS")); + assertEquals( + DriverEnvironment.OsType.OS_TYPE_UNKNOWN, DriverEnvironmentBuilder.detectOsType(null)); + } + + @Test + public void testGetMajorOsVersion() { + assertEquals("10", DriverEnvironmentBuilder.getMajorOsVersion("10.0")); + assertEquals("6", DriverEnvironmentBuilder.getMajorOsVersion("6.1.0")); + assertEquals("5", DriverEnvironmentBuilder.getMajorOsVersion("5")); + assertEquals("unknown", DriverEnvironmentBuilder.getMajorOsVersion(null)); + assertEquals("unknown", DriverEnvironmentBuilder.getMajorOsVersion(" ")); + } + + @Test + public void testGetOrCreateTelemetryTag_CreateNew(@TempDir Path tempDir) { + Path tagFile = tempDir.resolve("telemetry-tag"); + String tag = DriverEnvironmentBuilder.getOrCreateTelemetryTag(tagFile); + + assertNotNull(tag); + assertTrue(Files.exists(tagFile)); + // Verify it is a valid UUID + assertEquals(tag, UUID.fromString(tag).toString()); + } + + @Test + public void testGetOrCreateTelemetryTag_ReadExisting(@TempDir Path tempDir) throws IOException { + Path tagFile = tempDir.resolve("telemetry-tag"); + String existingUuid = UUID.randomUUID().toString(); + Files.write(tagFile, existingUuid.getBytes(StandardCharsets.UTF_8)); + + String tag = DriverEnvironmentBuilder.getOrCreateTelemetryTag(tagFile); + assertEquals(existingUuid, tag); + } + + @Test + public void testGetOrCreateTelemetryTag_RegenerateOnCorruptedFile(@TempDir Path tempDir) + throws IOException { + Path tagFile = tempDir.resolve("telemetry-tag"); + String corruptedContent = "not-a-valid-uuid"; + Files.write(tagFile, corruptedContent.getBytes(StandardCharsets.UTF_8)); + + String newTag = DriverEnvironmentBuilder.getOrCreateTelemetryTag(tagFile); + + assertNotNull(newTag); + assertNotEquals(corruptedContent, newTag); + assertEquals(newTag, UUID.fromString(newTag).toString()); + // Verify file on disk was overwritten with new valid UUID + String fileOnDisk = new String(Files.readAllBytes(tagFile), StandardCharsets.UTF_8).trim(); + assertEquals(newTag, fileOnDisk); + } +}