Implement IronTelemetry Java SDK

- Core client with exception/message capture
- Journey and step tracking with callable support
- Breadcrumb management with LinkedList ring buffer
- HTTP transport using OkHttp
- Full stack trace capture from throwables
- Thread-safe operations with synchronized blocks
- Sample rate and beforeSend filtering
- Tags, extras, and user context
- Async support with CompletableFuture
- Builder pattern for complex objects
This commit is contained in:
David Friedel 2025-12-25 10:23:14 +00:00
parent d72f0af6fd
commit 087bdd0bbe
21 changed files with 1959 additions and 2 deletions

48
.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Gradle
.gradle/
build/
!gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
# IDE
.idea/
*.iml
*.ipr
*.iws
.project
.classpath
.settings/
.vscode/
*.swp
*.swo
out/
bin/
# OS
.DS_Store
Thumbs.db
# Logs
*.log
logs/
# Package files
*.jar
*.war
*.ear
*.zip
*.tar.gz
*.rar

279
README.md
View File

@ -1,2 +1,277 @@
# irontelemetry-java # IronTelemetry SDK for Java
IronTelemetry SDK for Java - Error monitoring and crash reporting
Error monitoring and crash reporting SDK for Java applications. Capture exceptions, track user journeys, and get insights to fix issues faster.
[![Maven Central](https://img.shields.io/maven-central/v/com.ironservices/telemetry.svg)](https://search.maven.org/artifact/com.ironservices/telemetry)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## Installation
### Maven
```xml
<dependency>
<groupId>com.ironservices</groupId>
<artifactId>telemetry</artifactId>
<version>0.1.0</version>
</dependency>
```
### Gradle
```groovy
implementation 'com.ironservices:telemetry:0.1.0'
```
## Quick Start
### Basic Exception Capture
```java
import com.ironservices.telemetry.TelemetryClient;
public class Main {
public static void main(String[] args) {
// Initialize with your DSN
try (TelemetryClient client = new TelemetryClient("https://pk_live_xxx@irontelemetry.com")) {
try {
doSomething();
} catch (Exception e) {
client.captureException(e);
throw e;
}
}
}
}
```
### Journey Tracking
Track user journeys to understand the context of errors:
```java
import com.ironservices.telemetry.*;
public class CheckoutService {
private final TelemetryClient client;
public CheckoutService(TelemetryClient client) {
this.client = client;
}
public void processCheckout() {
Journey journey = client.startJourney("Checkout Flow");
journey.setUser("user-123", "user@example.com", "John Doe");
try {
journey.runStep("Validate Cart", BreadcrumbCategory.BUSINESS, () -> {
validateCart();
});
journey.runStep("Process Payment", BreadcrumbCategory.BUSINESS, () -> {
processPayment();
});
journey.runStep("Send Confirmation", BreadcrumbCategory.NOTIFICATION, () -> {
sendConfirmationEmail();
});
journey.complete();
} catch (Exception e) {
journey.fail(e);
client.captureException(e);
throw e;
}
}
}
```
## Configuration
```java
import com.ironservices.telemetry.TelemetryClient;
import com.ironservices.telemetry.TelemetryOptions;
TelemetryOptions options = new TelemetryOptions("https://pk_live_xxx@irontelemetry.com")
.setEnvironment("production")
.setAppVersion("1.2.3")
.setSampleRate(1.0) // 100% of events
.setDebug(false)
.setBeforeSend(event -> {
// Filter or modify events
if (event.getMessage() != null && event.getMessage().contains("expected")) {
return null; // Drop the event
}
return event;
});
TelemetryClient client = new TelemetryClient(options);
```
### Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `dsn` | String | required | Your Data Source Name |
| `environment` | String | "production" | Environment name |
| `appVersion` | String | "0.0.0" | Application version |
| `sampleRate` | double | 1.0 | Sample rate (0.0 to 1.0) |
| `maxBreadcrumbs` | int | 100 | Max breadcrumbs to keep |
| `debug` | boolean | false | Enable debug logging |
| `beforeSend` | Function | null | Hook to filter/modify events |
| `enableOfflineQueue` | boolean | true | Enable offline queue |
| `maxOfflineQueueSize` | int | 500 | Max offline queue size |
## Features
- **Automatic Stack Traces**: Full stack traces captured with every exception
- **Journey Tracking**: Track user flows and correlate errors with context
- **Breadcrumbs**: Leave a trail of events leading up to an error
- **User Context**: Associate errors with specific users
- **Tags & Extras**: Add custom metadata to your events
- **Async Support**: CompletableFuture support for async operations
- **Thread-Safe**: All operations are safe for concurrent use
## Breadcrumbs
```java
// Add simple breadcrumbs
client.addBreadcrumb("User clicked checkout button", BreadcrumbCategory.UI);
client.addBreadcrumb("Payment API called", BreadcrumbCategory.HTTP);
// With level
client.addBreadcrumb("User logged in", BreadcrumbCategory.AUTH, SeverityLevel.INFO);
// With data
Map<String, Object> data = new HashMap<>();
data.put("url", "/api/checkout");
data.put("statusCode", 200);
data.put("duration", 150);
client.addBreadcrumb("API request completed", BreadcrumbCategory.HTTP, SeverityLevel.INFO, data);
// Using builder
client.addBreadcrumb(Breadcrumb.builder("Payment processed", BreadcrumbCategory.BUSINESS)
.level(SeverityLevel.INFO)
.addData("amount", 99.99)
.addData("currency", "USD")
.build());
```
### Breadcrumb Categories
```java
BreadcrumbCategory.UI // User interface interactions
BreadcrumbCategory.HTTP // HTTP requests
BreadcrumbCategory.NAVIGATION // Page/route navigation
BreadcrumbCategory.CONSOLE // Console output
BreadcrumbCategory.AUTH // Authentication events
BreadcrumbCategory.BUSINESS // Business logic events
BreadcrumbCategory.NOTIFICATION // Notification events
BreadcrumbCategory.CUSTOM // Custom events
```
## Severity Levels
```java
SeverityLevel.DEBUG
SeverityLevel.INFO
SeverityLevel.WARNING
SeverityLevel.ERROR
SeverityLevel.FATAL
```
## User Context
```java
// Simple user ID
client.setUser("user-123");
// With email
client.setUser("user-123", "user@example.com");
// Full user object
client.setUser(User.builder("user-123")
.email("user@example.com")
.name("John Doe")
.addData("plan", "premium")
.build());
```
## Tags and Extra Data
```java
// Set individual tags
client.setTag("release", "v1.2.3");
client.setTag("server", "prod-1");
// Set multiple tags
Map<String, String> tags = new HashMap<>();
tags.put("release", "v1.2.3");
tags.put("server", "prod-1");
client.setTags(tags);
// Set extra data
client.setExtra("request_id", "abc-123");
Map<String, Object> extras = new HashMap<>();
extras.put("request_id", "abc-123");
extras.put("user_agent", "Mozilla/5.0...");
client.setExtras(extras);
```
## Async Operations
```java
// Async exception capture
CompletableFuture<SendResult> future = client.captureExceptionAsync(exception);
future.thenAccept(result -> {
if (result.isSuccess()) {
System.out.println("Event sent: " + result.getEventId());
}
});
// Async message capture
client.captureMessageAsync("Something happened", SeverityLevel.WARNING)
.thenAccept(result -> System.out.println("Sent: " + result.isSuccess()));
```
## Spring Integration Example
```java
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
private final TelemetryClient telemetryClient;
public GlobalExceptionHandler(TelemetryClient telemetryClient) {
this.telemetryClient = telemetryClient;
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleException(Exception e, HttpServletRequest request) {
telemetryClient.addBreadcrumb(
"HTTP Request: " + request.getMethod() + " " + request.getRequestURI(),
BreadcrumbCategory.HTTP
);
telemetryClient.captureException(e);
return ResponseEntity.status(500).body("Internal Server Error");
}
}
```
## Requirements
- Java 11+
- OkHttp 4.x
- Gson 2.x
## Links
- [Documentation](https://www.irontelemetry.com/docs)
- [Dashboard](https://www.irontelemetry.com)
## License
MIT License - see [LICENSE](LICENSE) for details.

106
pom.xml Normal file
View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ironservices</groupId>
<artifactId>telemetry</artifactId>
<version>0.1.0</version>
<packaging>jar</packaging>
<name>IronTelemetry</name>
<description>Error monitoring and crash reporting SDK for Java applications</description>
<url>https://github.com/IronServices/irontelemetry-java</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://opensource.org/licenses/MIT</url>
</license>
</licenses>
<developers>
<developer>
<name>IronServices</name>
<email>support@ironservices.com</email>
<organization>IronServices</organization>
<organizationUrl>https://www.ironservices.com</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/IronServices/irontelemetry-java.git</connection>
<developerConnection>scm:git:ssh://github.com:IronServices/irontelemetry-java.git</developerConnection>
<url>https://github.com/IronServices/irontelemetry-java</url>
</scm>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<gson.version>2.10.1</gson.version>
<okhttp.version>4.12.0</okhttp.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.3</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,85 @@
package com.ironservices.telemetry;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
/**
* Represents a breadcrumb event leading up to an error.
*/
public class Breadcrumb {
private final Instant timestamp;
private final BreadcrumbCategory category;
private final String message;
private final SeverityLevel level;
private final Map<String, Object> data;
private Breadcrumb(Builder builder) {
this.timestamp = builder.timestamp != null ? builder.timestamp : Instant.now();
this.category = builder.category;
this.message = builder.message;
this.level = builder.level != null ? builder.level : SeverityLevel.INFO;
this.data = builder.data;
}
public Instant getTimestamp() {
return timestamp;
}
public BreadcrumbCategory getCategory() {
return category;
}
public String getMessage() {
return message;
}
public SeverityLevel getLevel() {
return level;
}
public Map<String, Object> getData() {
return data;
}
public static Builder builder(String message, BreadcrumbCategory category) {
return new Builder(message, category);
}
public static class Builder {
private Instant timestamp;
private final BreadcrumbCategory category;
private final String message;
private SeverityLevel level;
private Map<String, Object> data = new HashMap<>();
public Builder(String message, BreadcrumbCategory category) {
this.message = message;
this.category = category;
}
public Builder timestamp(Instant timestamp) {
this.timestamp = timestamp;
return this;
}
public Builder level(SeverityLevel level) {
this.level = level;
return this;
}
public Builder data(Map<String, Object> data) {
this.data = data;
return this;
}
public Builder addData(String key, Object value) {
this.data.put(key, value);
return this;
}
public Breadcrumb build() {
return new Breadcrumb(this);
}
}
}

View File

@ -0,0 +1,30 @@
package com.ironservices.telemetry;
/**
* Category for breadcrumbs.
*/
public enum BreadcrumbCategory {
UI("ui"),
HTTP("http"),
NAVIGATION("navigation"),
CONSOLE("console"),
AUTH("auth"),
BUSINESS("business"),
NOTIFICATION("notification"),
CUSTOM("custom");
private final String value;
BreadcrumbCategory(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
}
}

View File

@ -0,0 +1,80 @@
package com.ironservices.telemetry;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Manages a ring buffer of breadcrumbs.
*/
public class BreadcrumbManager {
private final LinkedList<Breadcrumb> breadcrumbs;
private final int maxBreadcrumbs;
private final Object lock = new Object();
public BreadcrumbManager(int maxBreadcrumbs) {
this.breadcrumbs = new LinkedList<>();
this.maxBreadcrumbs = maxBreadcrumbs;
}
/**
* Add a breadcrumb to the ring buffer.
*/
public void add(Breadcrumb breadcrumb) {
synchronized (lock) {
if (breadcrumbs.size() >= maxBreadcrumbs) {
breadcrumbs.removeFirst();
}
breadcrumbs.add(breadcrumb);
}
}
/**
* Add a simple breadcrumb with just a message and category.
*/
public void add(String message, BreadcrumbCategory category) {
add(Breadcrumb.builder(message, category).build());
}
/**
* Add a breadcrumb with a specific level.
*/
public void add(String message, BreadcrumbCategory category, SeverityLevel level) {
add(Breadcrumb.builder(message, category).level(level).build());
}
/**
* Add a breadcrumb with additional data.
*/
public void add(String message, BreadcrumbCategory category, SeverityLevel level, Map<String, Object> data) {
add(Breadcrumb.builder(message, category).level(level).data(data).build());
}
/**
* Get all breadcrumbs as a copy.
*/
public List<Breadcrumb> getAll() {
synchronized (lock) {
return new ArrayList<>(breadcrumbs);
}
}
/**
* Clear all breadcrumbs.
*/
public void clear() {
synchronized (lock) {
breadcrumbs.clear();
}
}
/**
* Get the number of breadcrumbs.
*/
public int size() {
synchronized (lock) {
return breadcrumbs.size();
}
}
}

View File

@ -0,0 +1,43 @@
package com.ironservices.telemetry;
import java.util.ArrayList;
import java.util.List;
/**
* Represents exception/error information.
*/
public class ExceptionInfo {
private final String type;
private final String message;
private final List<StackFrame> stacktrace;
public ExceptionInfo(String type, String message, List<StackFrame> stacktrace) {
this.type = type;
this.message = message;
this.stacktrace = stacktrace;
}
public static ExceptionInfo fromThrowable(Throwable throwable) {
List<StackFrame> frames = new ArrayList<>();
for (StackTraceElement element : throwable.getStackTrace()) {
frames.add(new StackFrame(element));
}
return new ExceptionInfo(
throwable.getClass().getName(),
throwable.getMessage(),
frames
);
}
public String getType() {
return type;
}
public String getMessage() {
return message;
}
public List<StackFrame> getStacktrace() {
return stacktrace;
}
}

View File

@ -0,0 +1,123 @@
package com.ironservices.telemetry;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* Represents an active journey.
*/
public class Journey {
private final JourneyManager manager;
private final JourneyContext context;
Journey(JourneyManager manager, JourneyContext context) {
this.manager = manager;
this.context = context;
}
/**
* Set the user for the journey.
*/
public Journey setUser(String id, String email, String name) {
context.setMetadata("userId", id);
if (email != null) {
context.setMetadata("userEmail", email);
}
if (name != null) {
context.setMetadata("userName", name);
}
return this;
}
/**
* Set metadata for the journey.
*/
public Journey setMetadata(String key, Object value) {
context.setMetadata(key, value);
return this;
}
/**
* Start a new step in the journey.
*/
public Step startStep(String name, BreadcrumbCategory category) {
context.setCurrentStep(name);
TelemetryClient client = manager.getClient();
if (client != null) {
Map<String, Object> data = new HashMap<>();
data.put("journeyId", context.getJourneyId());
data.put("journeyName", context.getName());
client.addBreadcrumb("Started step: " + name, category, SeverityLevel.INFO, data);
}
return new Step(this, name, category);
}
/**
* Run a function as a step.
*/
public <T> T runStep(String name, BreadcrumbCategory category, Callable<T> fn) throws Exception {
Step step = startStep(name, category);
try {
T result = fn.call();
step.complete();
return result;
} catch (Exception e) {
step.fail(e);
throw e;
}
}
/**
* Run a void function as a step.
*/
public void runStep(String name, BreadcrumbCategory category, Runnable fn) {
Step step = startStep(name, category);
try {
fn.run();
step.complete();
} catch (Exception e) {
step.fail(e);
throw e;
}
}
/**
* Complete the journey successfully.
*/
public void complete() {
TelemetryClient client = manager.getClient();
if (client != null) {
Map<String, Object> data = new HashMap<>();
data.put("journeyId", context.getJourneyId());
data.put("duration", Duration.between(context.getStartedAt(), Instant.now()).toMillis());
client.addBreadcrumb("Completed journey: " + context.getName(), BreadcrumbCategory.BUSINESS, SeverityLevel.INFO, data);
}
manager.clear();
}
/**
* Mark the journey as failed.
*/
public void fail(Throwable error) {
TelemetryClient client = manager.getClient();
if (client != null) {
Map<String, Object> data = new HashMap<>();
data.put("journeyId", context.getJourneyId());
data.put("duration", Duration.between(context.getStartedAt(), Instant.now()).toMillis());
if (error != null) {
data.put("error", error.getMessage());
}
client.addBreadcrumb("Failed journey: " + context.getName(), BreadcrumbCategory.BUSINESS, SeverityLevel.ERROR, data);
}
manager.clear();
}
JourneyContext getContext() {
return context;
}
}

View File

@ -0,0 +1,51 @@
package com.ironservices.telemetry;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
/**
* Represents journey context for tracking user flows.
*/
public class JourneyContext {
private final String journeyId;
private final String name;
private String currentStep;
private final Instant startedAt;
private final Map<String, Object> metadata;
public JourneyContext(String journeyId, String name) {
this.journeyId = journeyId;
this.name = name;
this.startedAt = Instant.now();
this.metadata = new HashMap<>();
}
public String getJourneyId() {
return journeyId;
}
public String getName() {
return name;
}
public String getCurrentStep() {
return currentStep;
}
public void setCurrentStep(String currentStep) {
this.currentStep = currentStep;
}
public Instant getStartedAt() {
return startedAt;
}
public Map<String, Object> getMetadata() {
return metadata;
}
public void setMetadata(String key, Object value) {
this.metadata.put(key, value);
}
}

View File

@ -0,0 +1,64 @@
package com.ironservices.telemetry;
import java.util.Map;
import java.util.UUID;
import java.util.WeakHashMap;
/**
* Manages journey context.
*/
public class JourneyManager {
private static final Map<Journey, TelemetryClient> journeyClients = new WeakHashMap<>();
private final TelemetryClient client;
private volatile JourneyContext current;
private final Object lock = new Object();
public JourneyManager(TelemetryClient client) {
this.client = client;
}
/**
* Start a new journey.
*/
public Journey startJourney(String name) {
synchronized (lock) {
JourneyContext context = new JourneyContext(UUID.randomUUID().toString(), name);
this.current = context;
if (client != null) {
client.addBreadcrumb("Started journey: " + name, BreadcrumbCategory.BUSINESS);
}
Journey journey = new Journey(this, context);
journeyClients.put(journey, client);
return journey;
}
}
/**
* Get the current journey context.
*/
public JourneyContext getCurrent() {
synchronized (lock) {
return current;
}
}
/**
* Clear the current journey.
*/
public void clear() {
synchronized (lock) {
current = null;
}
}
TelemetryClient getClient() {
return client;
}
static TelemetryClient getClientForJourney(Journey journey) {
return journeyClients.get(journey);
}
}

View File

@ -0,0 +1,69 @@
package com.ironservices.telemetry;
import java.net.URI;
import java.net.URISyntaxException;
/**
* Represents parsed DSN components.
*/
public class ParsedDSN {
private final String publicKey;
private final String host;
private final String protocol;
private final String apiBaseUrl;
private ParsedDSN(String publicKey, String host, String protocol, String apiBaseUrl) {
this.publicKey = publicKey;
this.host = host;
this.protocol = protocol;
this.apiBaseUrl = apiBaseUrl;
}
/**
* Parse a DSN string.
* Format: https://pk_live_xxx@irontelemetry.com
*/
public static ParsedDSN parse(String dsn) {
if (dsn == null || dsn.isEmpty()) {
throw new IllegalArgumentException("DSN cannot be null or empty");
}
try {
URI uri = new URI(dsn);
String userInfo = uri.getUserInfo();
if (userInfo == null || !userInfo.startsWith("pk_")) {
throw new IllegalArgumentException("DSN must contain a valid public key starting with pk_");
}
String protocol = uri.getScheme();
String host = uri.getHost();
int port = uri.getPort();
String apiBaseUrl = protocol + "://" + host;
if (port > 0 && port != 80 && port != 443) {
apiBaseUrl += ":" + port;
}
return new ParsedDSN(userInfo, host, protocol, apiBaseUrl);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid DSN format: " + dsn, e);
}
}
public String getPublicKey() {
return publicKey;
}
public String getHost() {
return host;
}
public String getProtocol() {
return protocol;
}
public String getApiBaseUrl() {
return apiBaseUrl;
}
}

View File

@ -0,0 +1,36 @@
package com.ironservices.telemetry;
/**
* Represents platform/runtime information.
*/
public class PlatformInfo {
private final String name;
private final String version;
private final String os;
public PlatformInfo(String name, String version, String os) {
this.name = name;
this.version = version;
this.os = os;
}
public static PlatformInfo current() {
return new PlatformInfo(
"java",
System.getProperty("java.version"),
System.getProperty("os.name") + " " + System.getProperty("os.version")
);
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
public String getOs() {
return os;
}
}

View File

@ -0,0 +1,46 @@
package com.ironservices.telemetry;
/**
* Represents the result of sending an event.
*/
public class SendResult {
private final boolean success;
private final String eventId;
private final String error;
private final boolean queued;
private SendResult(boolean success, String eventId, String error, boolean queued) {
this.success = success;
this.eventId = eventId;
this.error = error;
this.queued = queued;
}
public static SendResult success(String eventId) {
return new SendResult(true, eventId, null, false);
}
public static SendResult failure(String error) {
return new SendResult(false, null, error, false);
}
public static SendResult queued(String eventId) {
return new SendResult(true, eventId, null, true);
}
public boolean isSuccess() {
return success;
}
public String getEventId() {
return eventId;
}
public String getError() {
return error;
}
public boolean isQueued() {
return queued;
}
}

View File

@ -0,0 +1,27 @@
package com.ironservices.telemetry;
/**
* Severity level for telemetry events.
*/
public enum SeverityLevel {
DEBUG("debug"),
INFO("info"),
WARNING("warning"),
ERROR("error"),
FATAL("fatal");
private final String value;
SeverityLevel(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
}
}

View File

@ -0,0 +1,41 @@
package com.ironservices.telemetry;
/**
* Represents a single frame in a stack trace.
*/
public class StackFrame {
private final String function;
private final String filename;
private final int lineno;
private final int colno;
public StackFrame(String function, String filename, int lineno, int colno) {
this.function = function;
this.filename = filename;
this.lineno = lineno;
this.colno = colno;
}
public StackFrame(StackTraceElement element) {
this.function = element.getClassName() + "." + element.getMethodName();
this.filename = element.getFileName();
this.lineno = element.getLineNumber();
this.colno = 0;
}
public String getFunction() {
return function;
}
public String getFilename() {
return filename;
}
public int getLineno() {
return lineno;
}
public int getColno() {
return colno;
}
}

View File

@ -0,0 +1,69 @@
package com.ironservices.telemetry;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
/**
* Represents a step within a journey.
*/
public class Step {
private final Journey journey;
private final String name;
private final BreadcrumbCategory category;
private final Instant startedAt;
private final Map<String, Object> data;
Step(Journey journey, String name, BreadcrumbCategory category) {
this.journey = journey;
this.name = name;
this.category = category;
this.startedAt = Instant.now();
this.data = new HashMap<>();
}
/**
* Set data for the step.
*/
public Step setData(String key, Object value) {
data.put(key, value);
return this;
}
/**
* Complete the step successfully.
*/
public void complete() {
TelemetryClient client = journey.getContext() != null ?
JourneyManager.getClientForJourney(journey) : null;
if (client != null) {
Map<String, Object> breadcrumbData = new HashMap<>();
breadcrumbData.put("journeyId", journey.getContext().getJourneyId());
breadcrumbData.put("journeyName", journey.getContext().getName());
breadcrumbData.put("duration", Duration.between(startedAt, Instant.now()).toMillis());
breadcrumbData.putAll(data);
client.addBreadcrumb("Completed step: " + name, category, SeverityLevel.INFO, breadcrumbData);
}
}
/**
* Mark the step as failed.
*/
public void fail(Throwable error) {
TelemetryClient client = JourneyManager.getClientForJourney(journey);
if (client != null) {
Map<String, Object> breadcrumbData = new HashMap<>();
breadcrumbData.put("journeyId", journey.getContext().getJourneyId());
breadcrumbData.put("journeyName", journey.getContext().getName());
breadcrumbData.put("duration", Duration.between(startedAt, Instant.now()).toMillis());
breadcrumbData.putAll(data);
if (error != null) {
breadcrumbData.put("error", error.getMessage());
}
client.addBreadcrumb("Failed step: " + name, category, SeverityLevel.ERROR, breadcrumbData);
}
}
}

View File

@ -0,0 +1,262 @@
package com.ironservices.telemetry;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
/**
* The main IronTelemetry client.
*/
public class TelemetryClient implements AutoCloseable {
private final TelemetryOptions options;
private final ParsedDSN parsedDSN;
private final Transport transport;
private final BreadcrumbManager breadcrumbs;
private final JourneyManager journeys;
private volatile User user;
private final Map<String, String> tags;
private final Map<String, Object> extra;
private final Object lock = new Object();
/**
* Create a new TelemetryClient with a DSN.
*/
public TelemetryClient(String dsn) {
this(new TelemetryOptions(dsn));
}
/**
* Create a new TelemetryClient with options.
*/
public TelemetryClient(TelemetryOptions options) {
this.options = options;
this.parsedDSN = ParsedDSN.parse(options.getDsn());
String apiBaseUrl = options.getApiBaseUrl() != null ? options.getApiBaseUrl() : parsedDSN.getApiBaseUrl();
this.transport = new Transport(parsedDSN, apiBaseUrl, options.isDebug());
this.breadcrumbs = new BreadcrumbManager(options.getMaxBreadcrumbs());
this.journeys = new JourneyManager(this);
this.tags = new HashMap<>();
this.extra = new HashMap<>();
if (options.isDebug()) {
System.out.println("[IronTelemetry] Initialized with DSN: " + parsedDSN.getApiBaseUrl());
}
}
/**
* Capture an exception and send it to the server.
*/
public SendResult captureException(Throwable throwable) {
if (throwable == null) {
return SendResult.failure("null exception");
}
TelemetryEvent event = createEvent(SeverityLevel.ERROR, throwable.getMessage());
event.setException(ExceptionInfo.fromThrowable(throwable));
return sendEvent(event);
}
/**
* Capture an exception asynchronously.
*/
public CompletableFuture<SendResult> captureExceptionAsync(Throwable throwable) {
return CompletableFuture.supplyAsync(() -> captureException(throwable));
}
/**
* Capture a message and send it to the server.
*/
public SendResult captureMessage(String message, SeverityLevel level) {
TelemetryEvent event = createEvent(level, message);
return sendEvent(event);
}
/**
* Capture a message with INFO level.
*/
public SendResult captureMessage(String message) {
return captureMessage(message, SeverityLevel.INFO);
}
/**
* Capture a message asynchronously.
*/
public CompletableFuture<SendResult> captureMessageAsync(String message, SeverityLevel level) {
return CompletableFuture.supplyAsync(() -> captureMessage(message, level));
}
/**
* Add a breadcrumb.
*/
public void addBreadcrumb(String message, BreadcrumbCategory category) {
breadcrumbs.add(message, category);
}
/**
* Add a breadcrumb with a level.
*/
public void addBreadcrumb(String message, BreadcrumbCategory category, SeverityLevel level) {
breadcrumbs.add(message, category, level);
}
/**
* Add a breadcrumb with data.
*/
public void addBreadcrumb(String message, BreadcrumbCategory category, SeverityLevel level, Map<String, Object> data) {
breadcrumbs.add(message, category, level, data);
}
/**
* Add a breadcrumb object.
*/
public void addBreadcrumb(Breadcrumb breadcrumb) {
breadcrumbs.add(breadcrumb);
}
/**
* Set the user context.
*/
public void setUser(User user) {
this.user = user;
}
/**
* Set the user context by ID.
*/
public void setUser(String id) {
this.user = User.builder(id).build();
}
/**
* Set the user context with ID and email.
*/
public void setUser(String id, String email) {
this.user = User.builder(id).email(email).build();
}
/**
* Set a tag.
*/
public void setTag(String key, String value) {
synchronized (lock) {
tags.put(key, value);
}
}
/**
* Set multiple tags.
*/
public void setTags(Map<String, String> tags) {
synchronized (lock) {
this.tags.putAll(tags);
}
}
/**
* Set extra data.
*/
public void setExtra(String key, Object value) {
synchronized (lock) {
extra.put(key, value);
}
}
/**
* Set multiple extra data values.
*/
public void setExtras(Map<String, Object> extras) {
synchronized (lock) {
this.extra.putAll(extras);
}
}
/**
* Start a new journey.
*/
public Journey startJourney(String name) {
return journeys.startJourney(name);
}
/**
* Get the current journey context.
*/
public JourneyContext getCurrentJourney() {
return journeys.getCurrent();
}
/**
* Clear all breadcrumbs.
*/
public void clearBreadcrumbs() {
breadcrumbs.clear();
}
/**
* Flush pending events.
*/
public void flush() {
// Future: Implement offline queue flushing
}
/**
* Close the client and release resources.
*/
@Override
public void close() {
flush();
transport.close();
}
private TelemetryEvent createEvent(SeverityLevel level, String message) {
TelemetryEvent event = new TelemetryEvent(
UUID.randomUUID().toString(),
level,
message
);
event.setEnvironment(options.getEnvironment());
event.setAppVersion(options.getAppVersion());
event.setBreadcrumbs(breadcrumbs.getAll());
synchronized (lock) {
event.setTags(new HashMap<>(tags));
event.setExtra(new HashMap<>(extra));
}
if (user != null) {
event.setUser(user);
}
JourneyContext journey = journeys.getCurrent();
if (journey != null) {
event.setJourney(journey);
}
return event;
}
private SendResult sendEvent(TelemetryEvent event) {
// Check sample rate
if (options.getSampleRate() < 1.0 && ThreadLocalRandom.current().nextDouble() > options.getSampleRate()) {
if (options.isDebug()) {
System.out.println("[IronTelemetry] Event sampled out: " + event.getEventId());
}
return SendResult.success(event.getEventId());
}
// Apply beforeSend hook
if (options.getBeforeSend() != null) {
event = options.getBeforeSend().apply(event);
if (event == null) {
if (options.isDebug()) {
System.out.println("[IronTelemetry] Event dropped by beforeSend hook");
}
return SendResult.success(null);
}
}
return transport.send(event);
}
}

View File

@ -0,0 +1,125 @@
package com.ironservices.telemetry;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents a telemetry event payload sent to the server.
*/
public class TelemetryEvent {
private final String eventId;
private final Instant timestamp;
private final SeverityLevel level;
private final String message;
private ExceptionInfo exception;
private User user;
private Map<String, String> tags;
private Map<String, Object> extra;
private List<Breadcrumb> breadcrumbs;
private JourneyContext journey;
private String environment;
private String appVersion;
private PlatformInfo platform;
public TelemetryEvent(String eventId, SeverityLevel level, String message) {
this.eventId = eventId;
this.timestamp = Instant.now();
this.level = level;
this.message = message;
this.tags = new HashMap<>();
this.extra = new HashMap<>();
this.breadcrumbs = new ArrayList<>();
this.platform = PlatformInfo.current();
}
public String getEventId() {
return eventId;
}
public Instant getTimestamp() {
return timestamp;
}
public SeverityLevel getLevel() {
return level;
}
public String getMessage() {
return message;
}
public ExceptionInfo getException() {
return exception;
}
public void setException(ExceptionInfo exception) {
this.exception = exception;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Map<String, String> getTags() {
return tags;
}
public void setTags(Map<String, String> tags) {
this.tags = tags;
}
public Map<String, Object> getExtra() {
return extra;
}
public void setExtra(Map<String, Object> extra) {
this.extra = extra;
}
public List<Breadcrumb> getBreadcrumbs() {
return breadcrumbs;
}
public void setBreadcrumbs(List<Breadcrumb> breadcrumbs) {
this.breadcrumbs = breadcrumbs;
}
public JourneyContext getJourney() {
return journey;
}
public void setJourney(JourneyContext journey) {
this.journey = journey;
}
public String getEnvironment() {
return environment;
}
public void setEnvironment(String environment) {
this.environment = environment;
}
public String getAppVersion() {
return appVersion;
}
public void setAppVersion(String appVersion) {
this.appVersion = appVersion;
}
public PlatformInfo getPlatform() {
return platform;
}
public void setPlatform(PlatformInfo platform) {
this.platform = platform;
}
}

View File

@ -0,0 +1,111 @@
package com.ironservices.telemetry;
import java.util.function.Function;
/**
* Configuration options for the telemetry client.
*/
public class TelemetryOptions {
private final String dsn;
private String environment = "production";
private String appVersion = "0.0.0";
private double sampleRate = 1.0;
private int maxBreadcrumbs = 100;
private boolean debug = false;
private Function<TelemetryEvent, TelemetryEvent> beforeSend;
private boolean enableOfflineQueue = true;
private int maxOfflineQueueSize = 500;
private String apiBaseUrl;
public TelemetryOptions(String dsn) {
if (dsn == null || dsn.isEmpty()) {
throw new IllegalArgumentException("DSN is required");
}
this.dsn = dsn;
}
public String getDsn() {
return dsn;
}
public String getEnvironment() {
return environment;
}
public TelemetryOptions setEnvironment(String environment) {
this.environment = environment;
return this;
}
public String getAppVersion() {
return appVersion;
}
public TelemetryOptions setAppVersion(String appVersion) {
this.appVersion = appVersion;
return this;
}
public double getSampleRate() {
return sampleRate;
}
public TelemetryOptions setSampleRate(double sampleRate) {
this.sampleRate = Math.max(0.0, Math.min(1.0, sampleRate));
return this;
}
public int getMaxBreadcrumbs() {
return maxBreadcrumbs;
}
public TelemetryOptions setMaxBreadcrumbs(int maxBreadcrumbs) {
this.maxBreadcrumbs = maxBreadcrumbs;
return this;
}
public boolean isDebug() {
return debug;
}
public TelemetryOptions setDebug(boolean debug) {
this.debug = debug;
return this;
}
public Function<TelemetryEvent, TelemetryEvent> getBeforeSend() {
return beforeSend;
}
public TelemetryOptions setBeforeSend(Function<TelemetryEvent, TelemetryEvent> beforeSend) {
this.beforeSend = beforeSend;
return this;
}
public boolean isEnableOfflineQueue() {
return enableOfflineQueue;
}
public TelemetryOptions setEnableOfflineQueue(boolean enableOfflineQueue) {
this.enableOfflineQueue = enableOfflineQueue;
return this;
}
public int getMaxOfflineQueueSize() {
return maxOfflineQueueSize;
}
public TelemetryOptions setMaxOfflineQueueSize(int maxOfflineQueueSize) {
this.maxOfflineQueueSize = maxOfflineQueueSize;
return this;
}
public String getApiBaseUrl() {
return apiBaseUrl;
}
public TelemetryOptions setApiBaseUrl(String apiBaseUrl) {
this.apiBaseUrl = apiBaseUrl;
return this;
}
}

View File

@ -0,0 +1,190 @@
package com.ironservices.telemetry;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import okhttp3.*;
import java.io.IOException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* Handles HTTP communication with the server.
*/
public class Transport {
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_INSTANT;
private final String apiBaseUrl;
private final String publicKey;
private final boolean debug;
private final OkHttpClient client;
private final Gson gson;
public Transport(ParsedDSN parsedDSN, String apiBaseUrl, boolean debug) {
this.apiBaseUrl = apiBaseUrl != null ? apiBaseUrl : parsedDSN.getApiBaseUrl();
this.publicKey = parsedDSN.getPublicKey();
this.debug = debug;
this.client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
this.gson = new GsonBuilder().serializeNulls().create();
}
/**
* Send an event synchronously.
*/
public SendResult send(TelemetryEvent event) {
String url = apiBaseUrl + "/api/v1/events";
try {
String body = gson.toJson(serializeEvent(event));
Request request = new Request.Builder()
.url(url)
.header("Content-Type", "application/json")
.header("X-Public-Key", publicKey)
.post(RequestBody.create(body, JSON))
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
String errorBody = response.body() != null ? response.body().string() : "";
String error = "HTTP " + response.code() + ": " + errorBody;
if (debug) {
System.out.println("[IronTelemetry] Failed to send event: " + error);
}
return SendResult.failure(error);
}
if (debug) {
System.out.println("[IronTelemetry] Event sent successfully: " + event.getEventId());
}
return SendResult.success(event.getEventId());
}
} catch (IOException e) {
if (debug) {
System.out.println("[IronTelemetry] Failed to send event: " + e.getMessage());
}
return SendResult.failure(e.getMessage());
}
}
/**
* Send an event asynchronously.
*/
public CompletableFuture<SendResult> sendAsync(TelemetryEvent event) {
return CompletableFuture.supplyAsync(() -> send(event));
}
/**
* Check if the server is reachable.
*/
public boolean isOnline() {
String url = apiBaseUrl + "/api/v1/health";
try {
Request request = new Request.Builder()
.url(url)
.header("X-Public-Key", publicKey)
.get()
.build();
try (Response response = client.newCall(request).execute()) {
return response.isSuccessful();
}
} catch (IOException e) {
return false;
}
}
private Map<String, Object> serializeEvent(TelemetryEvent event) {
Map<String, Object> result = new HashMap<>();
result.put("eventId", event.getEventId());
result.put("timestamp", formatInstant(event.getTimestamp()));
result.put("level", event.getLevel().getValue());
result.put("message", event.getMessage());
result.put("tags", event.getTags());
result.put("extra", event.getExtra());
result.put("environment", event.getEnvironment());
result.put("appVersion", event.getAppVersion());
// Breadcrumbs
List<Map<String, Object>> breadcrumbs = new ArrayList<>();
for (Breadcrumb b : event.getBreadcrumbs()) {
Map<String, Object> bc = new HashMap<>();
bc.put("timestamp", formatInstant(b.getTimestamp()));
bc.put("category", b.getCategory().getValue());
bc.put("message", b.getMessage());
bc.put("level", b.getLevel().getValue());
bc.put("data", b.getData());
breadcrumbs.add(bc);
}
result.put("breadcrumbs", breadcrumbs);
// Platform
if (event.getPlatform() != null) {
Map<String, Object> platform = new HashMap<>();
platform.put("name", event.getPlatform().getName());
platform.put("version", event.getPlatform().getVersion());
platform.put("os", event.getPlatform().getOs());
result.put("platform", platform);
}
// Exception
if (event.getException() != null) {
Map<String, Object> exception = new HashMap<>();
exception.put("type", event.getException().getType());
exception.put("message", event.getException().getMessage());
List<Map<String, Object>> stacktrace = new ArrayList<>();
for (StackFrame f : event.getException().getStacktrace()) {
Map<String, Object> frame = new HashMap<>();
frame.put("function", f.getFunction());
frame.put("filename", f.getFilename());
frame.put("lineno", f.getLineno());
stacktrace.add(frame);
}
exception.put("stacktrace", stacktrace);
result.put("exception", exception);
}
// User
if (event.getUser() != null) {
Map<String, Object> user = new HashMap<>();
user.put("id", event.getUser().getId());
user.put("email", event.getUser().getEmail());
user.put("name", event.getUser().getName());
user.put("data", event.getUser().getData());
result.put("user", user);
}
// Journey
if (event.getJourney() != null) {
Map<String, Object> journey = new HashMap<>();
journey.put("journeyId", event.getJourney().getJourneyId());
journey.put("name", event.getJourney().getName());
journey.put("currentStep", event.getJourney().getCurrentStep());
journey.put("startedAt", formatInstant(event.getJourney().getStartedAt()));
journey.put("metadata", event.getJourney().getMetadata());
result.put("journey", journey);
}
return result;
}
private String formatInstant(Instant instant) {
return ISO_FORMATTER.format(instant);
}
public void close() {
client.dispatcher().executorService().shutdown();
client.connectionPool().evictAll();
}
}

View File

@ -0,0 +1,76 @@
package com.ironservices.telemetry;
import java.util.HashMap;
import java.util.Map;
/**
* Represents user information for context.
*/
public class User {
private final String id;
private final String email;
private final String name;
private final Map<String, Object> data;
private User(Builder builder) {
this.id = builder.id;
this.email = builder.email;
this.name = builder.name;
this.data = builder.data;
}
public String getId() {
return id;
}
public String getEmail() {
return email;
}
public String getName() {
return name;
}
public Map<String, Object> getData() {
return data;
}
public static Builder builder(String id) {
return new Builder(id);
}
public static class Builder {
private final String id;
private String email;
private String name;
private Map<String, Object> data = new HashMap<>();
public Builder(String id) {
this.id = id;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder data(Map<String, Object> data) {
this.data = data;
return this;
}
public Builder addData(String key, Object value) {
this.data.put(key, value);
return this;
}
public User build() {
return new User(this);
}
}
}