Skip to content

Commit 7f49edd

Browse files
authored
feat(bigquery-jdbc): implement driver environment builder (#13723)
b/527947900 This is PR4 of multi-part telemetry client implementation. This PR implements `DriverEnvironmentBuilder` and its corresponding unit tests (`DriverEnvironmentBuilderTest`) as part of the BigQuery JDBC telemetry client infrastructure. It provides a package-private utility for constructing `DriverEnvironment` protocol buffer payloads by inspecting client runtime properties and managing a persistent installation-scoped UUID. #### Key Changes - **Environment Detection**: - Extracts major Java version (supporting both legacy `1.8.x` and modern `11+`/`17+` formats). - Maps OS names to standard `DriverEnvironment.OsType` enums (Windows, macOS/Darwin, Linux, Solaris, BSDs, AIX). - Extracts major OS version and sanitizes driver version strings (to `major.minor`). - **Telemetry Tag Management**: - Manages a persistent UUID in `~/.bigquery-jdbc/telemetry-tag`. - Includes defensive fallback logic: handles missing `user.home` (serverless/container runtimes), read-only file systems, `SecurityException`s, and automatically overwrites corrupted/invalid file contents with a fresh UUID.
1 parent fd23296 commit 7f49edd

3 files changed

Lines changed: 331 additions & 0 deletions

File tree

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.bigquery.jdbc.telemetry.v1;
18+
19+
import com.google.cloud.bigquery.jdbc.utils.BigQueryJdbcVersionUtility;
20+
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.nio.file.Paths;
25+
import java.util.UUID;
26+
import java.util.logging.Level;
27+
import java.util.logging.Logger;
28+
29+
/** Utility builder for constructing {@link DriverEnvironment} telemetry protos. */
30+
final class DriverEnvironmentBuilder {
31+
private static final Logger logger = Logger.getLogger(DriverEnvironmentBuilder.class.getName());
32+
33+
static final String DRIVER_NAME = "google-bigquery-jdbc-driver";
34+
static final String CLIENT_LANGUAGE = "java";
35+
static final String DEFAULT_TELEMETRY_TAG_DIR = ".bigquery-jdbc";
36+
static final String DEFAULT_TELEMETRY_TAG_FILE = "telemetry-tag";
37+
static final String UNKNOWN = "unknown";
38+
static final String RESTRICTED = "restricted";
39+
40+
private DriverEnvironmentBuilder() {}
41+
42+
static DriverEnvironment build() {
43+
return build(null);
44+
}
45+
46+
static DriverEnvironment build(Path customTelemetryTagPath) {
47+
return DriverEnvironment.newBuilder()
48+
.setDriverName(DRIVER_NAME)
49+
.setDriverVersion(BigQueryJdbcVersionUtility.getSanitizedDriverVersion())
50+
.setClientLanguage(CLIENT_LANGUAGE)
51+
.setClientLanguageVersion(getMajorJavaVersion())
52+
.setOsType(detectOsType())
53+
.setOsVersion(getMajorOsVersion())
54+
.setTelemetryTag(getOrCreateTelemetryTag(customTelemetryTagPath))
55+
.build();
56+
}
57+
58+
static String getMajorJavaVersion() {
59+
try {
60+
return getMajorJavaVersion(System.getProperty("java.version"));
61+
} catch (SecurityException e) {
62+
return RESTRICTED;
63+
}
64+
}
65+
66+
static String getMajorJavaVersion(String versionProperty) {
67+
if (versionProperty == null || versionProperty.trim().isEmpty()) {
68+
return UNKNOWN;
69+
}
70+
String version = versionProperty.trim();
71+
if (version.startsWith("1.")) {
72+
// Legacy Java version format (e.g. 1.8.0_292 -> 8)
73+
String[] parts = version.split("\\.");
74+
if (parts.length >= 2) {
75+
return parts[1];
76+
}
77+
} else {
78+
// Modern Java version format (e.g. 11.0.12 -> 11 or 17.0.1 -> 17)
79+
int firstDot = version.indexOf('.');
80+
if (firstDot > 0) {
81+
return version.substring(0, firstDot);
82+
}
83+
}
84+
return version;
85+
}
86+
87+
static DriverEnvironment.OsType detectOsType() {
88+
try {
89+
return detectOsType(System.getProperty("os.name"));
90+
} catch (SecurityException e) {
91+
return DriverEnvironment.OsType.OS_TYPE_UNSPECIFIED;
92+
}
93+
}
94+
95+
static DriverEnvironment.OsType detectOsType(String osNameProperty) {
96+
if (osNameProperty == null || osNameProperty.trim().isEmpty()) {
97+
return DriverEnvironment.OsType.OS_TYPE_UNKNOWN;
98+
}
99+
String osName = osNameProperty.toLowerCase();
100+
if (osName.contains("mac") || osName.contains("darwin")) {
101+
return DriverEnvironment.OsType.OS_TYPE_MACOS;
102+
} else if (osName.contains("win")) {
103+
return DriverEnvironment.OsType.OS_TYPE_WINDOWS;
104+
} else if (osName.contains("nux") || osName.contains("nix")) {
105+
return DriverEnvironment.OsType.OS_TYPE_LINUX;
106+
} else if (osName.contains("solaris") || osName.contains("sunos")) {
107+
return DriverEnvironment.OsType.OS_TYPE_SOLARIS;
108+
} else if (osName.contains("freebsd")) {
109+
return DriverEnvironment.OsType.OS_TYPE_FREEBSD;
110+
} else if (osName.contains("openbsd")) {
111+
return DriverEnvironment.OsType.OS_TYPE_OPENBSD;
112+
} else if (osName.contains("netbsd")) {
113+
return DriverEnvironment.OsType.OS_TYPE_NETBSD;
114+
} else if (osName.contains("aix")) {
115+
return DriverEnvironment.OsType.OS_TYPE_AIX;
116+
}
117+
return DriverEnvironment.OsType.OS_TYPE_UNKNOWN;
118+
}
119+
120+
static String getMajorOsVersion() {
121+
try {
122+
return getMajorOsVersion(System.getProperty("os.version"));
123+
} catch (SecurityException e) {
124+
return RESTRICTED;
125+
}
126+
}
127+
128+
static String getMajorOsVersion(String osVersionProperty) {
129+
if (osVersionProperty == null || osVersionProperty.trim().isEmpty()) {
130+
return UNKNOWN;
131+
}
132+
String version = osVersionProperty.trim();
133+
int firstDot = version.indexOf('.');
134+
if (firstDot > 0) {
135+
return version.substring(0, firstDot);
136+
}
137+
return version;
138+
}
139+
140+
static String getOrCreateTelemetryTag(Path customFilePath) {
141+
try {
142+
Path idFilePath = customFilePath;
143+
if (idFilePath == null) {
144+
String userHome = System.getProperty("user.home");
145+
if (userHome == null || userHome.trim().isEmpty()) {
146+
return UUID.randomUUID().toString();
147+
}
148+
idFilePath = Paths.get(userHome, DEFAULT_TELEMETRY_TAG_DIR, DEFAULT_TELEMETRY_TAG_FILE);
149+
}
150+
151+
if (Files.exists(idFilePath)) {
152+
try {
153+
String content =
154+
new String(Files.readAllBytes(idFilePath), StandardCharsets.UTF_8).trim();
155+
// Validate existing content is a valid UUID
156+
UUID.fromString(content);
157+
return content;
158+
} catch (Exception e) {
159+
logger.log(
160+
Level.WARNING, "Failed to read or parse telemetry tag from file, regenerating", e);
161+
}
162+
}
163+
164+
String newId = UUID.randomUUID().toString();
165+
try {
166+
if (idFilePath.getParent() != null) {
167+
Files.createDirectories(idFilePath.getParent());
168+
}
169+
Files.write(idFilePath, newId.getBytes(StandardCharsets.UTF_8));
170+
} catch (IOException e) {
171+
logger.log(Level.WARNING, "Failed to persist telemetry tag to file", e);
172+
}
173+
return newId;
174+
} catch (SecurityException e) {
175+
return UUID.randomUUID().toString();
176+
}
177+
}
178+
}

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/utils/BigQueryJdbcVersionUtility.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ public static String getDriverVersion() {
8080
return DRIVER_VERSION;
8181
}
8282

83+
/**
84+
* Returns a sanitized version of the driver version, containing only the major and minor
85+
* components (e.g. "1.2" for "1.2.0-SNAPSHOT").
86+
*/
87+
public static String getSanitizedDriverVersion() {
88+
return DRIVER_MAJOR_VERSION + "." + DRIVER_MINOR_VERSION;
89+
}
90+
8391
public static int getDriverMajorVersion() {
8492
return DRIVER_MAJOR_VERSION;
8593
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.bigquery.jdbc.telemetry.v1;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
21+
import static org.junit.jupiter.api.Assertions.assertNotNull;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
23+
24+
import java.io.IOException;
25+
import java.nio.charset.StandardCharsets;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import java.util.UUID;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.io.TempDir;
31+
32+
public class DriverEnvironmentBuilderTest {
33+
34+
@Test
35+
public void testBuildDriverEnvironment() {
36+
DriverEnvironment env = DriverEnvironmentBuilder.build();
37+
assertNotNull(env);
38+
assertEquals("google-bigquery-jdbc-driver", env.getDriverName());
39+
assertNotNull(env.getDriverVersion());
40+
assertEquals("java", env.getClientLanguage());
41+
assertNotNull(env.getClientLanguageVersion());
42+
assertNotNull(env.getOsType());
43+
assertNotNull(env.getOsVersion());
44+
assertNotNull(env.getTelemetryTag());
45+
}
46+
47+
@Test
48+
public void testBuildDriverEnvironmentCustomTagPath(@TempDir Path tempDir) {
49+
Path tagFile = tempDir.resolve("telemetry-tag");
50+
DriverEnvironment env = DriverEnvironmentBuilder.build(tagFile);
51+
assertNotNull(env);
52+
assertEquals("google-bigquery-jdbc-driver", env.getDriverName());
53+
assertNotNull(env.getDriverVersion());
54+
assertEquals("java", env.getClientLanguage());
55+
assertNotNull(env.getClientLanguageVersion());
56+
assertNotNull(env.getOsType());
57+
assertNotNull(env.getOsVersion());
58+
assertEquals(env.getTelemetryTag(), DriverEnvironmentBuilder.getOrCreateTelemetryTag(tagFile));
59+
}
60+
61+
@Test
62+
public void testGetMajorJavaVersion() {
63+
assertEquals("8", DriverEnvironmentBuilder.getMajorJavaVersion("1.8.0_292"));
64+
assertEquals("11", DriverEnvironmentBuilder.getMajorJavaVersion("11.0.12"));
65+
assertEquals("17", DriverEnvironmentBuilder.getMajorJavaVersion("17.0.1"));
66+
assertEquals("21", DriverEnvironmentBuilder.getMajorJavaVersion("21"));
67+
assertEquals("unknown", DriverEnvironmentBuilder.getMajorJavaVersion(null));
68+
assertEquals("unknown", DriverEnvironmentBuilder.getMajorJavaVersion(" "));
69+
}
70+
71+
@Test
72+
public void testDetectOsType() {
73+
assertEquals(
74+
DriverEnvironment.OsType.OS_TYPE_WINDOWS,
75+
DriverEnvironmentBuilder.detectOsType("Windows 11"));
76+
assertEquals(
77+
DriverEnvironment.OsType.OS_TYPE_MACOS, DriverEnvironmentBuilder.detectOsType("Mac OS X"));
78+
assertEquals(
79+
DriverEnvironment.OsType.OS_TYPE_MACOS, DriverEnvironmentBuilder.detectOsType("Darwin"));
80+
assertEquals(
81+
DriverEnvironment.OsType.OS_TYPE_LINUX, DriverEnvironmentBuilder.detectOsType("Linux"));
82+
assertEquals(
83+
DriverEnvironment.OsType.OS_TYPE_SOLARIS, DriverEnvironmentBuilder.detectOsType("Solaris"));
84+
assertEquals(
85+
DriverEnvironment.OsType.OS_TYPE_FREEBSD, DriverEnvironmentBuilder.detectOsType("FreeBSD"));
86+
assertEquals(
87+
DriverEnvironment.OsType.OS_TYPE_OPENBSD, DriverEnvironmentBuilder.detectOsType("OpenBSD"));
88+
assertEquals(
89+
DriverEnvironment.OsType.OS_TYPE_NETBSD, DriverEnvironmentBuilder.detectOsType("NetBSD"));
90+
assertEquals(
91+
DriverEnvironment.OsType.OS_TYPE_AIX, DriverEnvironmentBuilder.detectOsType("AIX"));
92+
assertEquals(
93+
DriverEnvironment.OsType.OS_TYPE_UNKNOWN,
94+
DriverEnvironmentBuilder.detectOsType("UnknownOS"));
95+
assertEquals(
96+
DriverEnvironment.OsType.OS_TYPE_UNKNOWN, DriverEnvironmentBuilder.detectOsType(null));
97+
}
98+
99+
@Test
100+
public void testGetMajorOsVersion() {
101+
assertEquals("10", DriverEnvironmentBuilder.getMajorOsVersion("10.0"));
102+
assertEquals("6", DriverEnvironmentBuilder.getMajorOsVersion("6.1.0"));
103+
assertEquals("5", DriverEnvironmentBuilder.getMajorOsVersion("5"));
104+
assertEquals("unknown", DriverEnvironmentBuilder.getMajorOsVersion(null));
105+
assertEquals("unknown", DriverEnvironmentBuilder.getMajorOsVersion(" "));
106+
}
107+
108+
@Test
109+
public void testGetOrCreateTelemetryTag_CreateNew(@TempDir Path tempDir) {
110+
Path tagFile = tempDir.resolve("telemetry-tag");
111+
String tag = DriverEnvironmentBuilder.getOrCreateTelemetryTag(tagFile);
112+
113+
assertNotNull(tag);
114+
assertTrue(Files.exists(tagFile));
115+
// Verify it is a valid UUID
116+
assertEquals(tag, UUID.fromString(tag).toString());
117+
}
118+
119+
@Test
120+
public void testGetOrCreateTelemetryTag_ReadExisting(@TempDir Path tempDir) throws IOException {
121+
Path tagFile = tempDir.resolve("telemetry-tag");
122+
String existingUuid = UUID.randomUUID().toString();
123+
Files.write(tagFile, existingUuid.getBytes(StandardCharsets.UTF_8));
124+
125+
String tag = DriverEnvironmentBuilder.getOrCreateTelemetryTag(tagFile);
126+
assertEquals(existingUuid, tag);
127+
}
128+
129+
@Test
130+
public void testGetOrCreateTelemetryTag_RegenerateOnCorruptedFile(@TempDir Path tempDir)
131+
throws IOException {
132+
Path tagFile = tempDir.resolve("telemetry-tag");
133+
String corruptedContent = "not-a-valid-uuid";
134+
Files.write(tagFile, corruptedContent.getBytes(StandardCharsets.UTF_8));
135+
136+
String newTag = DriverEnvironmentBuilder.getOrCreateTelemetryTag(tagFile);
137+
138+
assertNotNull(newTag);
139+
assertNotEquals(corruptedContent, newTag);
140+
assertEquals(newTag, UUID.fromString(newTag).toString());
141+
// Verify file on disk was overwritten with new valid UUID
142+
String fileOnDisk = new String(Files.readAllBytes(tagFile), StandardCharsets.UTF_8).trim();
143+
assertEquals(newTag, fileOnDisk);
144+
}
145+
}

0 commit comments

Comments
 (0)