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:
parent
d72f0af6fd
commit
087bdd0bbe
|
|
@ -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
279
README.md
|
|
@ -1,2 +1,277 @@
|
|||
# irontelemetry-java
|
||||
IronTelemetry SDK for Java - Error monitoring and crash reporting
|
||||
# IronTelemetry SDK for Java
|
||||
|
||||
Error monitoring and crash reporting SDK for Java applications. Capture exceptions, track user journeys, and get insights to fix issues faster.
|
||||
|
||||
[](https://search.maven.org/artifact/com.ironservices/telemetry)
|
||||
[](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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue