Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import dev.braintrust.log.BraintrustLogger;
import dev.braintrust.trace.BraintrustTracing;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter;
import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
Expand Down Expand Up @@ -43,7 +42,8 @@ public static void main(String[] args) throws Exception {

// NOTE: there are many ways to set up otel builders, etc.
// The important line is here: call enable with your otel builders and braintrust will export open telemetry data in addition to your existing setup
BraintrustTracing.enable(BraintrustConfig.fromEnvironment(), tracerBuilder, loggerBuilder, meterBuilder);
var braintrustConfig = BraintrustConfig.fromEnvironment();
BraintrustTracing.enable(braintrustConfig, tracerBuilder, loggerBuilder, meterBuilder);

var openTelemetry = OpenTelemetrySdk.builder()
.setTracerProvider(tracerBuilder.build())
Expand All @@ -63,7 +63,8 @@ public static void main(String[] args) throws Exception {
} finally {
span.end();
}
System.out.println("custom example completed!");
var url = braintrustConfig.fetchProjectURI() + "/logs?r=%s&s=%s".formatted(span.getSpanContext().getTraceId(), span.getSpanContext().getSpanId());
System.out.println("\n\nExample complete! View your data in Braintrust: " + url);
}

private static void registerShutdownHook(OpenTelemetrySdk otel) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ public static void main(String[] args) throws Exception {
Scorer.of("vegetable_scorer", result -> "vegetable".equals(result) ? 1.0 : 0.0))
.build();
var result = eval.run();
System.out.println(result.createReportString());
System.out.println("\n\n example complete! View your data in Braintrust: " + result.createReportString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.ChatModel;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import dev.braintrust.config.BraintrustConfig;
import dev.braintrust.instrumentation.openai.BraintrustOpenAI;
import dev.braintrust.trace.BraintrustTracing;

Expand All @@ -15,7 +16,8 @@ public static void main(String[] args) throws Exception {
if (null == System.getenv("OPENAI_API_KEY")) {
System.err.println("\nWARNING envar OPEN_AI_API_KEY not found. This example will likely fail.\n");
}
var openTelemetry = BraintrustTracing.quickstart();
var braintrustConfig = BraintrustConfig.fromEnvironment();
var openTelemetry = BraintrustTracing.quickstart(braintrustConfig, true);
var tracer = BraintrustTracing.getTracer(openTelemetry);
OpenAIClient openAIClient = BraintrustOpenAI.wrapOpenAI(openTelemetry, OpenAIOkHttpClient.fromEnv());
var rootSpan = tracer.spanBuilder("java-braintrust-example").startSpan();
Expand All @@ -36,6 +38,7 @@ public static void main(String[] args) throws Exception {
} finally {
rootSpan.end();
}
System.out.println("example completed!");
var url = braintrustConfig.fetchProjectURI() + "/logs?r=%s&s=%s".formatted(rootSpan.getSpanContext().getTraceId(), rootSpan.getSpanContext().getSpanId());
System.out.println("\n\nExample complete! View your data in Braintrust: " + url);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package dev.braintrust.examples;

import dev.braintrust.config.BraintrustConfig;
import dev.braintrust.trace.BraintrustTracing;

public class SimpleOpenTelemetryExample {
public static void main(String[] args) throws Exception {
var openTelemetry = BraintrustTracing.quickstart();
var braintrustConfig = BraintrustConfig.fromEnvironment();
var openTelemetry = BraintrustTracing.quickstart(braintrustConfig, true);
var tracer = BraintrustTracing.getTracer(openTelemetry);

var span = tracer.spanBuilder("hello-java").startSpan();
Expand All @@ -15,6 +17,7 @@ public static void main(String[] args) throws Exception {
} finally {
span.end();
}
System.out.println("simple example completed!");
var url = braintrustConfig.fetchProjectURI() + "/logs?r=%s&s=%s".formatted(span.getSpanContext().getTraceId(), span.getSpanContext().getSpanId());
System.out.println("\n\nExample complete! View your data in Braintrust: " + url);
}
}
118 changes: 57 additions & 61 deletions src/main/java/dev/braintrust/api/BraintrustApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,88 +36,82 @@ public BraintrustApiClient(BraintrustConfig config) {
}

/** Creates or gets a project by name. */
public CompletableFuture<Project> createProject(String name) {
var request = new CreateProjectRequest(name);
return postAsync("/v1/project", request, Project.class);
public Project getOrCreateProject(String projectName) {
try {
var request = new CreateProjectRequest(projectName);
return postAsync("/v1/project", request, Project.class).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}

/** Gets a project by ID. */
public CompletableFuture<Optional<Project>> getProject(String projectId) {
return getAsync("/v1/project/" + projectId, Project.class)
.handle(
(project, error) -> {
if (error != null && isNotFound(error)) {
return Optional.<Project>empty();
}
if (error != null) {
throw new CompletionException(error);
}
return Optional.of(project);
});
}

/** Lists all projects. */
public CompletableFuture<List<Project>> listProjects() {
return getAsync("/v1/project", ProjectList.class).thenApply(ProjectList::projects);
public Optional<Project> getProject(String projectId) {
try {
return getAsync("/v1/project/" + projectId, Project.class)
.handle(
(project, error) -> {
if (error != null && isNotFound(error)) {
return Optional.<Project>empty();
}
if (error != null) {
throw new CompletionException(error);
}
return Optional.of(project);
}).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}

/** Creates an experiment. */
public Experiment createExperiment(CreateExperimentRequest request) {
public Experiment getOrCreateExperiment(CreateExperimentRequest request) {
try {
return postAsync("/v1/experiment", request, Experiment.class).get();
} catch (InterruptedException | ExecutionException e) {
throw new ApiException(e);
}
}

/** Gets an experiment by ID. */
public CompletableFuture<Optional<Experiment>> getExperiment(String experimentId) {
return getAsync("/v1/experiment/" + experimentId, Experiment.class)
.handle(
(experiment, error) -> {
if (error != null && isNotFound(error)) {
return Optional.<Experiment>empty();
}
if (error != null) {
throw new CompletionException(error);
}
return Optional.of(experiment);
});
}

/** Lists experiments for a project. */
public CompletableFuture<List<Experiment>> listExperiments(String projectId) {
return getAsync("/v1/experiment?project_id=" + projectId, ExperimentList.class)
.thenApply(ExperimentList::experiments);
}

/** Creates a dataset. */
public CompletableFuture<Dataset> createDataset(CreateDatasetRequest request) {
return postAsync("/v1/dataset", request, Dataset.class);
/** Login to get user information including organization details. */
public LoginResponse login() {
try {
return postAsync("/api/apikey/login", new LoginRequest(config.apiKey()), LoginResponse.class).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}

/** Inserts events into a dataset. */
public CompletableFuture<InsertEventsResponse> insertDatasetEvents(
String datasetId, List<DatasetEvent> events) {
var request = new InsertEventsRequest(events);
return postAsync(
"/v1/dataset/" + datasetId + "/insert", request, InsertEventsResponse.class);
public Optional<OrganizationAndProjectInfo> getProjectAndOrgInfo() {
var projectId = config.defaultProjectId().orElse(null);
if (null == projectId) {
projectId = getOrCreateProject(config.defaultProjectName().orElseThrow()).id();
}
return getProjectAndOrgInfo(projectId);
}

/** Lists datasets for a project. */
public CompletableFuture<List<Dataset>> listDatasets(String projectId) {
return getAsync("/v1/dataset?project_id=" + projectId, DatasetList.class)
.thenApply(DatasetList::datasets);
public Optional<OrganizationAndProjectInfo> getOrCreateProjectAndOrgInfo(String projectName) {
return getProjectAndOrgInfo(getOrCreateProject(projectName).id());
}

/** Login to get user information including organization details. */
public CompletableFuture<LoginResponse> login() {
var request = new LoginRequest(config.apiKey());
return postAsync("/api/apikey/login", request, LoginResponse.class);
public Optional<OrganizationAndProjectInfo> getProjectAndOrgInfo(String projectId) {
var project = getProject(projectId).orElse(null);
if (null == project) {
return Optional.empty();
}
OrganizationInfo orgInfo = null;
for (var org : login().orgInfo()) {
if (project.orgId().equalsIgnoreCase(org.id())) {
orgInfo = org;
break;
}
}
if (null == orgInfo) {
throw new ApiException("Should not happen. Unable to find project's org: " + project.orgId());
}
return Optional.of(new OrganizationAndProjectInfo(orgInfo, project));
}

// Low-level HTTP methods

private <T> CompletableFuture<T> getAsync(String path, Class<T> responseType) {
var request =
HttpRequest.newBuilder()
Expand Down Expand Up @@ -273,4 +267,6 @@ public record OrganizationInfo(String id, String name) {}
private record LoginRequest(String token) {}

public record LoginResponse(List<OrganizationInfo> orgInfo) {}

public record OrganizationAndProjectInfo(OrganizationInfo orgInfo, Project project) {}
}
68 changes: 43 additions & 25 deletions src/main/java/dev/braintrust/config/BraintrustConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package dev.braintrust.config;

import dev.braintrust.api.BraintrustApiClient;

import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.util.Optional;
import java.util.function.Consumer;
Expand All @@ -11,7 +14,7 @@
* variable support.
*/
public final class BraintrustConfig {
public static final String FALLBACK_PROJECT_NAME = "default-java-project";
private static final String DEFAULT_PROJECT_NAME = "default-java-project";
private static final String DEFAULT_API_URL = "https://api.braintrust.dev";
private static final String DEFAULT_APP_URL = "https://www.braintrust.dev";

Expand All @@ -21,7 +24,7 @@ public final class BraintrustConfig {
private final String logsPath;
private final URI appUrl;
@Nullable private final String defaultProjectId;
@Nullable private final String orgName;
@Nullable private final String defaultProjectName;
private final boolean enableTraceConsoleLog;
private final boolean debug;
private final Duration requestTimeout;
Expand All @@ -35,7 +38,7 @@ private BraintrustConfig(Builder builder) {
this.appUrl = builder.appUrl;
this.experimentalOtelLogs = builder.experimentalOtelLogs;
this.defaultProjectId = builder.defaultProjectId;
this.orgName = builder.orgName;
this.defaultProjectName = builder.defaultProjectName;
this.enableTraceConsoleLog = builder.enableTraceConsoleLog;
this.debug = builder.debug;
this.requestTimeout = builder.requestTimeout;
Expand Down Expand Up @@ -65,8 +68,8 @@ public Optional<String> defaultProjectId() {
return Optional.ofNullable(defaultProjectId);
}

public Optional<String> orgName() {
return Optional.ofNullable(orgName);
public Optional<String> defaultProjectName() {
return Optional.ofNullable(defaultProjectName);
}

public boolean enableTraceConsoleLog() {
Expand All @@ -81,6 +84,25 @@ public Duration requestTimeout() {
return requestTimeout;
}

/**
* The parent attribute tells braintrust where to send otel data
* <br/><br/>
* The otel ingestion endpoint looks for (a) braintrust.parent = project_id|project_name|experiment_id:value otel attribute and routes accordingly
* <br/><br/>
* (b) if a span has no parent marked explicitly, it will look to see if there's an x-bt-parent http header (with the same format marked above e.g. project_name:andrew) that parent will apply to all spans in a request that don't have one
* <br/><br/>
* If neither (a) nor (b) exists, the data is dropped
*/
public Optional<String> getBraintrustParentValue() {
if (null != defaultProjectId) {
return Optional.of("project_id:" + defaultProjectId);
} else if (null != defaultProjectName) {
return Optional.of("project_name:" + defaultProjectName);
} else {
return Optional.empty();
}
}

/** Creates a new builder initialized with environment variables. */
public static Builder builder() {
return new Builder();
Expand All @@ -91,13 +113,14 @@ public static BraintrustConfig fromEnvironment() {
return builder().build();
}

/**
* Creates a config with a consumer for customization and retrieves organization name from API.
*/
public static BraintrustConfig create(Consumer<Builder> customizer) {
var builder = builder();
customizer.accept(builder);
return builder.build();
public URI fetchProjectURI() {
try {
var client = new BraintrustApiClient(this);
var orgAndProject = client.getProjectAndOrgInfo().orElseThrow();
return new URI(appUrl() + "/app/" + orgAndProject.orgInfo().name() + "/p/" + orgAndProject.project().name());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}

public static final class Builder {
Expand All @@ -107,21 +130,26 @@ public static final class Builder {
private String logsPath;
private URI appUrl;
private String defaultProjectId;
private String orgName;
private String defaultProjectName;
private boolean enableTraceConsoleLog;
private boolean debug;
private boolean experimentalOtelLogs;
private Duration requestTimeout = Duration.ofSeconds(30);

private Builder() {
// Initialize from environment
this.apiKey = getEnv("BRAINTRUST_API_KEY", null);
this.apiUrl = URI.create(getEnv("BRAINTRUST_API_URL", DEFAULT_API_URL));
this.tracesPath = getEnv("BRAINTRUST_TRACES_PATH", "/otel/v1/traces");
this.logsPath = getEnv("BRAINTRUST_LOGS_PATH", "/otel/v1/logs");
this.appUrl = URI.create(getEnv("BRAINTRUST_APP_URL", DEFAULT_APP_URL));
this.orgName = getEnv("BRAINTRUST_ORG_NAME", null);
this.defaultProjectId = getEnv("BRAINTRUST_DEFAULT_PROJECT_ID", null);
this.defaultProjectName = getEnv("BRAINTRUST_DEFAULT_PROJECT", DEFAULT_PROJECT_NAME).trim();
if ((null == defaultProjectId || "".equalsIgnoreCase(defaultProjectId.trim()))
&& "".equalsIgnoreCase(defaultProjectName)) {
// NOTE: this should not happen,
// but if someone happens to export their default project to the empty string and does not set a default project ID we don't have a valid parent for otel data.
throw new RuntimeException("Missing required envars. Please export BRAINTRUST_DEFAULT_PROJECT_ID or BRAINTRUST_DEFAULT_PROJECT");
}
this.enableTraceConsoleLog =
Boolean.parseBoolean(getEnv("BRAINTRUST_ENABLE_TRACE_CONSOLE_LOG", "false"));
this.debug = Boolean.parseBoolean(getEnv("BRAINTRUST_DEBUG", "false"));
Expand Down Expand Up @@ -156,11 +184,6 @@ public Builder defaultProjectId(String projectId) {
return this;
}

public Builder orgName(String orgName) {
this.orgName = orgName;
return this;
}

public Builder enableTraceConsoleLog(boolean enable) {
this.enableTraceConsoleLog = enable;
return this;
Expand All @@ -181,11 +204,6 @@ public BraintrustConfig build() {
throw new IllegalStateException("API key is required. Set BRAINTRUST_API_KEY environment variable or use apiKey() method.");
}

// If orgName is not already set, try to retrieve it from the API
if (orgName == null || orgName.isBlank()) {
throw new IllegalStateException("org name is required");
}

return new BraintrustConfig(this);
}

Expand Down
Loading
Loading