Skip to content

Commit debd0e3

Browse files
fix: telemetry fixes, auditToolCall method (#111)
* fix: derive SDK version from pom.properties and detect platform version in telemetry - Read SDK_VERSION from Maven pom.properties at runtime instead of hardcoded constant to prevent version drift between pom.xml and telemetry payload - Detect platform version by calling /health endpoint before sending telemetry ping (2s timeout, silent failure) * fix: use imported JsonNode instead of fully-qualified names * fix: normalize os and arch in telemetry payload Map 'Mac OS X' -> 'darwin', 'aarch64' -> 'arm64' for consistency across SDKs. * fix: update stale javadoc for sdkEndpoint parameter * fix: replace hardcoded version fallback with "unknown" Avoids manual drift risk when the version bumps but the fallback string is forgotten. pom.properties and manifest should always resolve in packaged JARs. * fix: guard compareSemver against unknown version, normalize amd64, use try-with-resources - Skip version compatibility warning when SDK_VERSION is "unknown" (prevents spurious warnings when running from source/IDE) - Add amd64 -> x64 mapping in normalizeArch for Linux consistency - Use try-with-resources for InputStream in detectSdkVersion * feat: add auditToolCall method (#1260) * fix: suppress telemetry for localhost endpoints When the SDK endpoint is localhost, 127.0.0.1, or [::1], telemetry pings are now suppressed unless telemetryEnabled is explicitly set to true. Prevents telemetry leaks during local development. Updated 4 tests to pass telemetryEnabled=true for WireMock localhost endpoints. * chore: bump version to 4.1.0 * fix: remove duplicate endpoint call in AuditToolCallTest * docs: add v4.1.0 changelog entry * docs: add v4.1.0 changelog entry * docs: set v4.1.0 release date
1 parent 89c9aab commit debd0e3

9 files changed

Lines changed: 879 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to the AxonFlow Java SDK will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [4.1.0] - 2026-03-14
9+
10+
### Added
11+
12+
- `auditToolCall()` — record non-LLM tool calls (API, MCP, function) in the audit trail. Returns audit ID, status, and timestamp. Requires Platform v5.1.0+
13+
- `getAuditLogsByTenant()` — retrieve audit logs for a tenant with optional pagination
14+
- `searchAuditLogs()` — search audit logs with filters (client ID, request type, limit)
15+
16+
### Fixed
17+
18+
- Telemetry pings now suppressed for localhost/127.0.0.1/[::1] endpoints unless `telemetryEnabled` is explicitly set to `true`. Prevents telemetry noise during local development.
19+
20+
---
21+
822
## [4.0.0] - 2026-03-09
923

1024
### Breaking Changes

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.getaxonflow</groupId>
88
<artifactId>axonflow-sdk</artifactId>
9-
<version>4.0.0</version>
9+
<version>4.1.0</version>
1010
<packaging>jar</packaging>
1111

1212
<name>AxonFlow Java SDK</name>

src/main/java/com/getaxonflow/sdk/AxonFlow.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ public HealthStatus healthCheck() {
261261

262262
if (status.getSdkCompatibility() != null
263263
&& status.getSdkCompatibility().getMinSdkVersion() != null
264+
&& !"unknown".equals(AxonFlowConfig.SDK_VERSION)
264265
&& compareSemver(AxonFlowConfig.SDK_VERSION, status.getSdkCompatibility().getMinSdkVersion()) < 0) {
265266
logger.warn("SDK version {} is below minimum supported version {}. Please upgrade.",
266267
AxonFlowConfig.SDK_VERSION, status.getSdkCompatibility().getMinSdkVersion());
@@ -597,6 +598,57 @@ public CompletableFuture<AuditSearchResponse> getAuditLogsByTenantAsync(String t
597598
return CompletableFuture.supplyAsync(() -> getAuditLogsByTenant(tenantId, options), asyncExecutor);
598599
}
599600

601+
// ========================================================================
602+
// Audit Tool Call
603+
// ========================================================================
604+
605+
/**
606+
* Audits a non-LLM tool call for compliance and observability.
607+
*
608+
* <p>Records tool invocations such as function calls, MCP operations,
609+
* or API calls to the audit log.
610+
*
611+
* <p>Example usage:
612+
* <pre>{@code
613+
* AuditToolCallResponse response = axonflow.auditToolCall(
614+
* AuditToolCallRequest.builder()
615+
* .toolName("web_search")
616+
* .toolType("function")
617+
* .input(Map.of("query", "latest news"))
618+
* .output(Map.of("results", 5))
619+
* .workflowId("wf_123")
620+
* .durationMs(450L)
621+
* .success(true)
622+
* .build());
623+
* }</pre>
624+
*
625+
* @param request the audit tool call request
626+
* @return the audit tool call response with audit ID
627+
* @throws NullPointerException if request is null
628+
* @throws IllegalArgumentException if tool_name is null or empty
629+
* @throws AxonFlowException if the audit fails
630+
*/
631+
public AuditToolCallResponse auditToolCall(AuditToolCallRequest request) {
632+
Objects.requireNonNull(request, "request cannot be null");
633+
634+
return retryExecutor.execute(() -> {
635+
Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/audit/tool-call", request);
636+
try (Response response = httpClient.newCall(httpRequest).execute()) {
637+
return parseResponse(response, AuditToolCallResponse.class);
638+
}
639+
}, "auditToolCall");
640+
}
641+
642+
/**
643+
* Asynchronously audits a non-LLM tool call.
644+
*
645+
* @param request the audit tool call request
646+
* @return a future containing the audit tool call response
647+
*/
648+
public CompletableFuture<AuditToolCallResponse> auditToolCallAsync(AuditToolCallRequest request) {
649+
return CompletableFuture.supplyAsync(() -> auditToolCall(request), asyncExecutor);
650+
}
651+
600652
// ========================================================================
601653
// Proxy Mode - Query Execution
602654
// ========================================================================

src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
import com.getaxonflow.sdk.util.RetryConfig;
2121
import com.getaxonflow.sdk.util.CacheConfig;
2222

23+
import java.io.InputStream;
2324
import java.time.Duration;
2425
import java.util.Objects;
26+
import java.util.Properties;
2527

2628
/**
2729
* Configuration for the AxonFlow client.
@@ -42,8 +44,32 @@
4244
*/
4345
public final class AxonFlowConfig {
4446

45-
/** SDK version string. */
46-
public static final String SDK_VERSION = "3.8.0";
47+
/** SDK version string, read from Maven pom.properties at runtime. */
48+
public static final String SDK_VERSION = detectSdkVersion();
49+
50+
private static String detectSdkVersion() {
51+
// Try Maven-generated pom.properties (available in packaged JAR)
52+
try (InputStream is = AxonFlowConfig.class.getResourceAsStream(
53+
"/META-INF/maven/com.getaxonflow/axonflow-sdk/pom.properties")) {
54+
if (is != null) {
55+
Properties props = new Properties();
56+
props.load(is);
57+
String version = props.getProperty("version");
58+
if (version != null && !version.isEmpty()) {
59+
return version;
60+
}
61+
}
62+
} catch (Exception ignored) {
63+
// Fall through to manifest check
64+
}
65+
// Try JAR manifest Implementation-Version
66+
Package pkg = AxonFlowConfig.class.getPackage();
67+
if (pkg != null && pkg.getImplementationVersion() != null) {
68+
return pkg.getImplementationVersion();
69+
}
70+
// Fallback — "unknown" avoids hardcoded version drift
71+
return "unknown";
72+
}
4773

4874
/** Default timeout for HTTP requests. */
4975
public static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(60);

src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.getaxonflow.sdk.telemetry;
1717

1818
import com.getaxonflow.sdk.AxonFlowConfig;
19+
import com.fasterxml.jackson.databind.JsonNode;
1920
import com.fasterxml.jackson.databind.ObjectMapper;
2021
import com.fasterxml.jackson.databind.node.ArrayNode;
2122
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -59,7 +60,7 @@ public class TelemetryReporter {
5960
* Sends an anonymous telemetry ping asynchronously (fire-and-forget).
6061
*
6162
* @param mode the deployment mode (e.g. "production", "sandbox")
62-
* @param sdkEndpoint the configured SDK endpoint (unused in payload, present for future use)
63+
* @param sdkEndpoint the configured SDK endpoint, used to detect platform version via /health
6364
* @param telemetryEnabled config override for telemetry (null = use default based on mode)
6465
* @param debug whether debug logging is enabled
6566
*/
@@ -84,15 +85,25 @@ static void sendPing(String mode, String sdkEndpoint, Boolean telemetryEnabled,
8485
return;
8586
}
8687

88+
// Suppress telemetry for localhost endpoints unless explicitly enabled.
89+
if (!Boolean.TRUE.equals(telemetryEnabled) && isLocalhostEndpoint(sdkEndpoint)) {
90+
if (debug) {
91+
logger.debug("Telemetry suppressed for localhost endpoint");
92+
}
93+
return;
94+
}
95+
8796
logger.info("AxonFlow: anonymous telemetry enabled. Opt out: AXONFLOW_TELEMETRY=off | https://docs.getaxonflow.com/telemetry");
8897

8998
String endpoint = (checkpointUrl != null && !checkpointUrl.isEmpty())
9099
? checkpointUrl
91100
: DEFAULT_ENDPOINT;
92101

102+
final String finalSdkEndpoint = sdkEndpoint;
93103
CompletableFuture.runAsync(() -> {
94104
try {
95-
String payload = buildPayload(mode);
105+
String platformVersion = detectPlatformVersion(finalSdkEndpoint);
106+
String payload = buildPayload(mode, platformVersion);
96107

97108
OkHttpClient client = new OkHttpClient.Builder()
98109
.connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
@@ -162,15 +173,19 @@ static boolean isEnabled(String mode, Boolean configOverride, boolean hasCredent
162173
/**
163174
* Builds the JSON payload for the telemetry ping.
164175
*/
165-
static String buildPayload(String mode) {
176+
static String buildPayload(String mode, String platformVersion) {
166177
try {
167178
ObjectMapper mapper = new ObjectMapper();
168179
ObjectNode root = mapper.createObjectNode();
169180
root.put("sdk", "java");
170181
root.put("sdk_version", AxonFlowConfig.SDK_VERSION);
171-
root.putNull("platform_version");
172-
root.put("os", System.getProperty("os.name"));
173-
root.put("arch", System.getProperty("os.arch"));
182+
if (platformVersion != null) {
183+
root.put("platform_version", platformVersion);
184+
} else {
185+
root.putNull("platform_version");
186+
}
187+
root.put("os", normalizeOS(System.getProperty("os.name")));
188+
root.put("arch", normalizeArch(System.getProperty("os.arch")));
174189
root.put("runtime_version", System.getProperty("java.version"));
175190
root.put("deployment_mode", mode);
176191

@@ -186,6 +201,76 @@ static String buildPayload(String mode) {
186201
}
187202
}
188203

204+
/**
205+
* Detect platform version by calling the agent's /health endpoint.
206+
* Returns null on any failure.
207+
*/
208+
static String detectPlatformVersion(String sdkEndpoint) {
209+
if (sdkEndpoint == null || sdkEndpoint.isEmpty()) {
210+
return null;
211+
}
212+
try {
213+
OkHttpClient client = new OkHttpClient.Builder()
214+
.connectTimeout(2, TimeUnit.SECONDS)
215+
.readTimeout(2, TimeUnit.SECONDS)
216+
.build();
217+
218+
Request request = new Request.Builder()
219+
.url(sdkEndpoint + "/health")
220+
.get()
221+
.build();
222+
223+
try (Response response = client.newCall(request).execute()) {
224+
if (response.isSuccessful() && response.body() != null) {
225+
ObjectMapper mapper = new ObjectMapper();
226+
JsonNode root = mapper.readTree(response.body().string());
227+
JsonNode versionNode = root.get("version");
228+
if (versionNode != null && !versionNode.isNull() && !versionNode.asText().isEmpty()) {
229+
return versionNode.asText();
230+
}
231+
}
232+
}
233+
} catch (Exception ignored) {
234+
// Silent failure
235+
}
236+
return null;
237+
}
238+
239+
/**
240+
* Normalize OS name to lowercase short form consistent across SDKs.
241+
* e.g. "Mac OS X" -> "darwin", "Windows 10" -> "windows", "Linux" -> "linux"
242+
*/
243+
static String normalizeOS(String osName) {
244+
if (osName == null) return "unknown";
245+
String lower = osName.toLowerCase();
246+
if (lower.contains("mac") || lower.contains("darwin")) return "darwin";
247+
if (lower.contains("win")) return "windows";
248+
if (lower.contains("linux")) return "linux";
249+
return lower;
250+
}
251+
252+
/**
253+
* Normalize arch name consistent across SDKs.
254+
* e.g. "aarch64" -> "arm64", "x86_64" -> "x64"
255+
*/
256+
static String normalizeArch(String arch) {
257+
if (arch == null) return "unknown";
258+
if ("aarch64".equals(arch)) return "arm64";
259+
if ("x86_64".equals(arch) || "amd64".equals(arch)) return "x64";
260+
return arch;
261+
}
262+
263+
/**
264+
* Check whether the endpoint is a localhost address.
265+
*/
266+
static boolean isLocalhostEndpoint(String endpoint) {
267+
if (endpoint == null || endpoint.isEmpty()) {
268+
return false;
269+
}
270+
String lower = endpoint.toLowerCase();
271+
return lower.contains("localhost") || lower.contains("127.0.0.1") || lower.contains("[::1]");
272+
}
273+
189274
private TelemetryReporter() {
190275
// Utility class
191276
}

0 commit comments

Comments
 (0)