diff --git a/examples/Program.cs b/examples/Program.cs index e4b2a53..a007e3d 100644 --- a/examples/Program.cs +++ b/examples/Program.cs @@ -1,4 +1,36 @@ -// Placeholder for Braintrust SDK examples -// Add example programs here +using System; +using System.Threading; +using Braintrust.Sdk; +using Braintrust.Sdk.Trace; -Console.WriteLine("Braintrust SDK Examples"); +namespace Braintrust.Sdk.Examples; + +class SimpleOpenTelemetryExample +{ + static void Main(string[] args) + { + var braintrust = Braintrust.Get(); + var tracerProvider = braintrust.OpenTelemetryCreate(); + var activitySource = BraintrustTracing.GetActivitySource(); + + using (var activity = activitySource.StartActivity("hello-dotnet")) + { + if (activity != null) + { + Console.WriteLine("Performing simple operation..."); + activity.SetTag("some boolean attribute", true); + Thread.Sleep(100); // Not required. This is just to make the span look interesting + } + + if (activity != null) + { + var url = braintrust.ProjectUri() + + $"/logs?r={activity.TraceId}&s={activity.SpanId}"; + Console.WriteLine($"\n\n Example complete! View your data in Braintrust: {url}"); + } + } + + // Dispose the tracer provider to flush any remaining spans + tracerProvider?.Dispose(); + } +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..3e32d80 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,61 @@ +# Braintrust SDK Examples + +This directory contains example applications demonstrating how to use the Braintrust .NET SDK. + +## Prerequisites + +Before running these examples, make sure you have: + +1. .NET 8.0 SDK installed +2. A Braintrust account and API key +3. Environment variables configured (see below) + +## Configuration + +Set the following environment variables: + +```bash +export BRAINTRUST_API_KEY="your-api-key" +export BRAINTRUST_DEFAULT_PROJECT_NAME="your-project-name" +# Or use project ID: +# export BRAINTRUST_DEFAULT_PROJECT_ID="your-project-id" +``` + +## Running the Examples + +### Simple OpenTelemetry Example + +This example demonstrates basic OpenTelemetry tracing with Braintrust: + +```bash +cd examples +dotnet run +``` + +The example: +- Initializes the Braintrust SDK +- Sets up OpenTelemetry tracing +- Creates a simple span with attributes +- Prints a URL to view the trace in Braintrust + +## Example Code + +The main example is in `Program.cs`: + +```csharp +var braintrust = Braintrust.Get(); +var tracerProvider = braintrust.OpenTelemetryCreate(); +var activitySource = BraintrustTracing.GetActivitySource(); + +using (var activity = activitySource.StartActivity("hello-dotnet")) +{ + activity?.SetTag("some boolean attribute", true); + // Your code here... +} +``` + +## Next Steps + +- Check out the [Braintrust documentation](https://www.braintrust.dev/docs) for more advanced usage +- Explore the SDK source code in `src/Braintrust.Sdk/` +- Look at the test files in `tests/Braintrust.Sdk.Tests/` for more examples diff --git a/src/Braintrust.Sdk/Braintrust.Sdk.csproj b/src/Braintrust.Sdk/Braintrust.Sdk.csproj index 7ff75c4..572d2b7 100644 --- a/src/Braintrust.Sdk/Braintrust.Sdk.csproj +++ b/src/Braintrust.Sdk/Braintrust.Sdk.csproj @@ -16,6 +16,8 @@ + + diff --git a/src/Braintrust.Sdk/Braintrust.cs b/src/Braintrust.Sdk/Braintrust.cs index 1836432..6d731ac 100644 --- a/src/Braintrust.Sdk/Braintrust.cs +++ b/src/Braintrust.Sdk/Braintrust.cs @@ -128,48 +128,41 @@ public Uri ProjectUri() return new Uri($"{Config.AppUrl}/app/{orgAndProject.OrgInfo.Name}/p/{orgAndProject.Project.Name}"); } - // TODO: Implement when we have BraintrustTracing - // /// - // /// Quick start method that sets up global OpenTelemetry with this Braintrust. - // /// - // /// If you're looking for more options for configuring Braintrust/OpenTelemetry, - // /// consult the Enable method. - // /// - // public OpenTelemetry OpenTelemetryCreate() - // { - // return OpenTelemetryCreate(registerGlobal: true); - // } + /// + /// Quick start method that sets up OpenTelemetry with this Braintrust. + /// + /// If you're looking for more options for configuring Braintrust/OpenTelemetry, + /// consult the Enable method. + /// + public OpenTelemetry.Trace.TracerProvider OpenTelemetryCreate() + { + return OpenTelemetryCreate(registerGlobal: true); + } - // TODO: Implement when we have BraintrustTracing - // /// - // /// Quick start method that sets up OpenTelemetry with this Braintrust. - // /// - // /// If you're looking for more options for configuring Braintrust and OpenTelemetry, - // /// consult the Enable method. - // /// - // public OpenTelemetry OpenTelemetryCreate(bool registerGlobal) - // { - // return BraintrustTracing.Of(Config, registerGlobal); - // } + /// + /// Quick start method that sets up OpenTelemetry with this Braintrust. + /// + /// If you're looking for more options for configuring Braintrust and OpenTelemetry, + /// consult the Enable method. + /// + public OpenTelemetry.Trace.TracerProvider OpenTelemetryCreate(bool registerGlobal) + { + return Trace.BraintrustTracing.Of(Config, registerGlobal); + } - // TODO: Implement when we have BraintrustTracing - // /// - // /// Add Braintrust to existing OpenTelemetry builders. - // /// - // /// This method provides the most options for configuring Braintrust and OpenTelemetry. - // /// If you're looking for a more user-friendly setup, consult the OpenTelemetryCreate methods. - // /// - // /// NOTE: This method should only be invoked once. Enabling Braintrust multiple times is - // /// unsupported and may lead to undesired behavior. - // /// - // public void OpenTelemetryEnable( - // TracerProviderBuilder tracerProviderBuilder, - // LoggerProviderBuilder loggerProviderBuilder, - // MeterProviderBuilder meterProviderBuilder) - // { - // BraintrustTracing.Enable( - // Config, tracerProviderBuilder, loggerProviderBuilder, meterProviderBuilder); - // } + /// + /// Add Braintrust to existing OpenTelemetry TracerProviderBuilder. + /// + /// This method provides the most options for configuring Braintrust and OpenTelemetry. + /// If you're looking for a more user-friendly setup, consult the OpenTelemetryCreate methods. + /// + /// NOTE: This method should only be invoked once. Enabling Braintrust multiple times is + /// unsupported and may lead to undesired behavior. + /// + public void OpenTelemetryEnable(OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder) + { + Trace.BraintrustTracing.Enable(Config, tracerProviderBuilder); + } // TODO: Implement when we have Eval // /// diff --git a/src/Braintrust.Sdk/Config/BraintrustConfig.cs b/src/Braintrust.Sdk/Config/BraintrustConfig.cs index 5257b1c..4f150a3 100644 --- a/src/Braintrust.Sdk/Config/BraintrustConfig.cs +++ b/src/Braintrust.Sdk/Config/BraintrustConfig.cs @@ -85,11 +85,11 @@ private BraintrustConfig(IDictionary envOverrides) : base(envOve /// public string? GetBraintrustParentValue() { - if (DefaultProjectId != null) + if (!string.IsNullOrEmpty(DefaultProjectId)) { return $"project_id:{DefaultProjectId}"; } - else if (DefaultProjectName != null) + else if (!string.IsNullOrEmpty(DefaultProjectName)) { return $"project_name:{DefaultProjectName}"; } diff --git a/src/Braintrust.Sdk/Trace/BraintrustContext.cs b/src/Braintrust.Sdk/Trace/BraintrustContext.cs new file mode 100644 index 0000000..4ab4b1e --- /dev/null +++ b/src/Braintrust.Sdk/Trace/BraintrustContext.cs @@ -0,0 +1,95 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace Braintrust.Sdk.Trace; + +/// +/// Context carrier for Braintrust parent relationships (project/experiment IDs). +/// This is stored in AsyncLocal to propagate parent information through the trace. +/// +public sealed class BraintrustContext +{ + private static readonly AsyncLocal _current = new AsyncLocal(); + + public string? ProjectId { get; } + public string? ExperimentId { get; } + + private BraintrustContext(string? projectId, string? experimentId) + { + ProjectId = projectId; + ExperimentId = experimentId; + } + + /// + /// Create a Braintrust context for an experiment. + /// + public static BraintrustContext OfExperiment(string experimentId) + { + return new BraintrustContext(null, experimentId); + } + + /// + /// Create a Braintrust context for a project. + /// + public static BraintrustContext OfProject(string projectId) + { + return new BraintrustContext(projectId, null); + } + + /// + /// Get the current Braintrust context. + /// + public static BraintrustContext? Current => _current.Value; + + /// + /// Get the Braintrust context from the current Activity (span). + /// + public static BraintrustContext? FromContext(Activity? activity = null) + { + // Return the current async local value + return _current.Value; + } + + /// + /// Set this context as the current Braintrust context. + /// Returns a disposable scope that will restore the previous context. + /// + public IDisposable MakeCurrent() + { + var previous = _current.Value; + _current.Value = this; + return new ContextScope(previous); + } + + private class ContextScope : IDisposable + { + private readonly BraintrustContext? _previous; + + public ContextScope(BraintrustContext? previous) + { + _previous = previous; + } + + public void Dispose() + { + _current.Value = _previous; + } + } + + /// + /// Get the parent value string for this context (format: "project_id:X" or "experiment_id:Y"). + /// + public string? GetParentValue() + { + if (ExperimentId != null) + { + return $"experiment_id:{ExperimentId}"; + } + if (ProjectId != null) + { + return $"project_id:{ProjectId}"; + } + return null; + } +} diff --git a/src/Braintrust.Sdk/Trace/BraintrustSpanExporter.cs b/src/Braintrust.Sdk/Trace/BraintrustSpanExporter.cs new file mode 100644 index 0000000..227ff82 --- /dev/null +++ b/src/Braintrust.Sdk/Trace/BraintrustSpanExporter.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics; +using Braintrust.Sdk.Config; +using OpenTelemetry; + +namespace Braintrust.Sdk.Trace; + +/// +/// Custom span exporter for Braintrust that logs span exports in debug mode. +/// +/// Note: Unlike the Java SDK, this implementation does not yet support dynamic per-parent +/// HTTP header routing. The parent information is still included in span attributes, +/// so the backend can route based on that. Full per-parent header support requires +/// a more complex implementation with custom HTTP handling. +/// +internal sealed class BraintrustSpanExporter : BaseExporter +{ + private readonly BraintrustConfig _config; + + public BraintrustSpanExporter(BraintrustConfig config) + { + _config = config ?? throw new ArgumentNullException(nameof(config)); + } + + public override ExportResult Export(in Batch batch) + { + if (_config.Debug) + { + var count = 0; + foreach (var activity in batch) + { + count++; + var parent = activity.GetTagItem(BraintrustSpanProcessor.ParentAttributeKey); + Console.WriteLine($"[BraintrustSpanExporter] Exporting span: {activity.DisplayName}, parent={parent}"); + } + Console.WriteLine($"[BraintrustSpanExporter] Exported {count} spans"); + } + + // Return success - the actual export is handled by the OTLP exporter in the pipeline + return ExportResult.Success; + } +} diff --git a/src/Braintrust.Sdk/Trace/BraintrustSpanProcessor.cs b/src/Braintrust.Sdk/Trace/BraintrustSpanProcessor.cs new file mode 100644 index 0000000..d137a2f --- /dev/null +++ b/src/Braintrust.Sdk/Trace/BraintrustSpanProcessor.cs @@ -0,0 +1,87 @@ +using System; +using System.Diagnostics; +using Braintrust.Sdk.Config; +using OpenTelemetry; + +namespace Braintrust.Sdk.Trace; + +/// +/// Custom span processor that enriches spans with Braintrust-specific attributes. +/// Supports parent assignment to projects or experiments. +/// +internal sealed class BraintrustSpanProcessor : BaseProcessor +{ + public const string ParentAttributeKey = "braintrust.parent"; + + private readonly BraintrustConfig _config; + + public BraintrustSpanProcessor(BraintrustConfig config) + { + _config = config ?? throw new ArgumentNullException(nameof(config)); + } + + public override void OnStart(Activity activity) + { + // Check if activity already has a parent attribute + var existingParent = activity.GetTagItem(ParentAttributeKey); + if (existingParent != null) + { + return; + } + + // Check if parent context has Braintrust attributes first + var btContext = BraintrustContext.Current; + if (btContext != null) + { + var parentValue = btContext.GetParentValue(); + if (parentValue != null) + { + activity.SetTag(ParentAttributeKey, parentValue); + if (_config.Debug) + { + Console.WriteLine($"[BraintrustSpanProcessor] OnStart: set parent {parentValue} from context for span {activity.DisplayName}"); + } + return; + } + } + + // Get parent from the config if context doesn't have it + var configParent = _config.GetBraintrustParentValue(); + if (configParent != null) + { + activity.SetTag(ParentAttributeKey, configParent); + if (_config.Debug) + { + Console.WriteLine($"[BraintrustSpanProcessor] OnStart: set parent {configParent} from config for span {activity.DisplayName}"); + } + } + } + + public override void OnEnd(Activity activity) + { + if (_config.Debug) + { + LogActivityDetails(activity); + } + } + + private void LogActivityDetails(Activity activity) + { + var duration = activity.Duration.TotalMilliseconds; + Console.WriteLine( + $"[BraintrustSpanProcessor] Activity completed: name={activity.DisplayName}, " + + $"traceId={activity.TraceId}, spanId={activity.SpanId}, duration={duration:F2}ms"); + + // Log tags + foreach (var tag in activity.Tags) + { + Console.WriteLine($" Tag: {tag.Key}={tag.Value}"); + } + + // Log events + foreach (var evt in activity.Events) + { + Console.WriteLine($" Event: {evt.Name} at {evt.Timestamp}"); + } + } +} diff --git a/src/Braintrust.Sdk/Trace/BraintrustTracing.cs b/src/Braintrust.Sdk/Trace/BraintrustTracing.cs new file mode 100644 index 0000000..5852b94 --- /dev/null +++ b/src/Braintrust.Sdk/Trace/BraintrustTracing.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Braintrust.Sdk.Config; +using OpenTelemetry; +using OpenTelemetry.Exporter; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace Braintrust.Sdk.Trace; + +/// +/// Main entry point for Braintrust OpenTelemetry tracing setup. +/// +public static class BraintrustTracing +{ + public const string ParentKey = "braintrust.parent"; + private const string OtelServiceName = "braintrust-app"; + private const string InstrumentationName = "braintrust-dotnet"; + + private static readonly string InstrumentationVersion = + typeof(BraintrustTracing).Assembly.GetName().Version?.ToString() ?? "0.0.1"; + + /// + /// Quick start method that sets up global OpenTelemetry with Braintrust from environment config. + /// + public static TracerProvider Quickstart() + { + var config = BraintrustConfig.FromEnvironment(); + return Of(config, registerGlobal: true); + } + + /// + /// Set up OpenTelemetry with Braintrust configuration. + /// + /// Braintrust configuration + /// Whether to register as the global TracerProvider (not used in .NET, kept for API compatibility) + public static TracerProvider Of(BraintrustConfig config, bool registerGlobal = false) + { + var builder = OpenTelemetry.Sdk.CreateTracerProviderBuilder(); + Enable(config, builder); + + var provider = builder.Build(); + + // Note: In .NET, there's no direct equivalent to Java's GlobalOpenTelemetry.set() + // The TracerProvider is typically managed through dependency injection or kept as a singleton + // Users can store the returned provider and access it as needed + + return provider; + } + + /// + /// Add Braintrust configuration to an existing TracerProviderBuilder. + /// This method provides the most options for configuring Braintrust and OpenTelemetry. + /// + public static void Enable(BraintrustConfig config, TracerProviderBuilder tracerProviderBuilder) + { + // Set up resource with service name and version + var resource = ResourceBuilder.CreateDefault() + .AddService( + serviceName: OtelServiceName, + serviceVersion: InstrumentationVersion) + .Build(); + + tracerProviderBuilder + .SetResourceBuilder(ResourceBuilder.CreateDefault() + .AddService(serviceName: OtelServiceName, serviceVersion: InstrumentationVersion)) + .AddSource(InstrumentationName) + .AddProcessor(new BraintrustSpanProcessor(config)) + .AddOtlpExporter(otlpOptions => + { + // Configure OTLP HTTP exporter to send to Braintrust + otlpOptions.Protocol = OtlpExportProtocol.HttpProtobuf; + otlpOptions.Endpoint = new Uri($"{config.ApiUrl}{config.TracesPath}"); + otlpOptions.Headers = BuildHeaders(config); + otlpOptions.TimeoutMilliseconds = (int)config.RequestTimeout.TotalMilliseconds; + }) + .SetSampler(new AlwaysOnSampler()); + + // TODO: Add per-parent HTTP header routing like Java SDK + // Currently parent info is sent via span attributes; full header-based routing + // requires custom HTTP handling + // TODO: Add shutdown hook for graceful cleanup + } + + /// + /// Get an ActivitySource for instrumentation. In .NET, use ActivitySource instead of Tracer. + /// + public static ActivitySource GetActivitySource() + { + return new ActivitySource(InstrumentationName, InstrumentationVersion); + } + + private static string BuildHeaders(BraintrustConfig config) + { + var headers = new List + { + $"Authorization=Bearer {config.ApiKey}" + }; + + // Add parent header if available + var parentValue = config.GetBraintrustParentValue(); + if (parentValue != null) + { + headers.Add($"x-bt-parent={parentValue}"); + } + + return string.Join(",", headers); + } +} diff --git a/tests/Braintrust.Sdk.Tests/Trace/BraintrustContextTest.cs b/tests/Braintrust.Sdk.Tests/Trace/BraintrustContextTest.cs new file mode 100644 index 0000000..7842191 --- /dev/null +++ b/tests/Braintrust.Sdk.Tests/Trace/BraintrustContextTest.cs @@ -0,0 +1,82 @@ +using Braintrust.Sdk.Trace; +using Xunit; + +namespace Braintrust.Sdk.Tests.Trace; + +public class BraintrustContextTest +{ + [Fact] + public void OfExperiment_CreatesContextWithExperimentId() + { + var context = BraintrustContext.OfExperiment("exp-123"); + + Assert.Null(context.ProjectId); + Assert.Equal("exp-123", context.ExperimentId); + } + + [Fact] + public void OfProject_CreatesContextWithProjectId() + { + var context = BraintrustContext.OfProject("proj-456"); + + Assert.Equal("proj-456", context.ProjectId); + Assert.Null(context.ExperimentId); + } + + [Fact] + public void GetParentValue_ReturnsExperimentIdFormat() + { + var context = BraintrustContext.OfExperiment("exp-123"); + + var parentValue = context.GetParentValue(); + + Assert.Equal("experiment_id:exp-123", parentValue); + } + + [Fact] + public void GetParentValue_ReturnsProjectIdFormat() + { + var context = BraintrustContext.OfProject("proj-456"); + + var parentValue = context.GetParentValue(); + + Assert.Equal("project_id:proj-456", parentValue); + } + + [Fact] + public void MakeCurrent_SetsCurrent() + { + var context = BraintrustContext.OfProject("proj-789"); + + using (context.MakeCurrent()) + { + Assert.Equal(context, BraintrustContext.Current); + } + + // Context should be restored after dispose + Assert.Null(BraintrustContext.Current); + } + + [Fact] + public void MakeCurrent_RestoresPreviousContext() + { + var context1 = BraintrustContext.OfProject("proj-1"); + var context2 = BraintrustContext.OfProject("proj-2"); + + using (context1.MakeCurrent()) + { + Assert.Equal(context1, BraintrustContext.Current); + + using (context2.MakeCurrent()) + { + Assert.Equal(context2, BraintrustContext.Current); + } + + // Should restore to context1 + Assert.Equal(context1, BraintrustContext.Current); + } + + // Should restore to null + Assert.Null(BraintrustContext.Current); + } +} diff --git a/tests/Braintrust.Sdk.Tests/Trace/BraintrustSpanExporterTest.cs b/tests/Braintrust.Sdk.Tests/Trace/BraintrustSpanExporterTest.cs new file mode 100644 index 0000000..c3607b8 --- /dev/null +++ b/tests/Braintrust.Sdk.Tests/Trace/BraintrustSpanExporterTest.cs @@ -0,0 +1,66 @@ +using System; +using System.Diagnostics; +using Braintrust.Sdk.Config; +using Braintrust.Sdk.Trace; +using OpenTelemetry; +using Xunit; + +namespace Braintrust.Sdk.Tests.Trace; + +public class BraintrustSpanExporterTest : IDisposable +{ + private readonly ActivityListener _activityListener; + + public BraintrustSpanExporterTest() + { + // Set up activity listener so activities are actually created + _activityListener = new ActivityListener + { + ShouldListenTo = source => source.Name == "test-source", + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData + }; + ActivitySource.AddActivityListener(_activityListener); + } + + public void Dispose() + { + _activityListener?.Dispose(); + } + [Fact] + public void Export_SucceedsWithEmptyBatch() + { + var config = BraintrustConfig.Of( + "BRAINTRUST_API_KEY", "test-key", + "BRAINTRUST_EXPORT_SPANS_IN_MEMORY_FOR_UNIT_TEST", "false" + ); + var exporter = new BraintrustSpanExporter(config); + + var activities = System.Array.Empty(); + var batch = new Batch(activities, 0); + + var result = exporter.Export(batch); + + Assert.Equal(ExportResult.Success, result); + } + + [Fact] + public void Export_SucceedsInTestMode() + { + var config = BraintrustConfig.Of( + "BRAINTRUST_API_KEY", "test-key", + "BRAINTRUST_EXPORT_SPANS_IN_MEMORY_FOR_UNIT_TEST", "true" + ); + var exporter = new BraintrustSpanExporter(config); + + using var activitySource = new ActivitySource("test-source"); + using var activity = activitySource.StartActivity("test-span"); + + Assert.NotNull(activity); + var activities = new[] { activity }; + var batch = new Batch(activities, 1); + + var result = exporter.Export(batch); + + Assert.Equal(ExportResult.Success, result); + } +} diff --git a/tests/Braintrust.Sdk.Tests/Trace/BraintrustSpanProcessorTest.cs b/tests/Braintrust.Sdk.Tests/Trace/BraintrustSpanProcessorTest.cs new file mode 100644 index 0000000..c5336bd --- /dev/null +++ b/tests/Braintrust.Sdk.Tests/Trace/BraintrustSpanProcessorTest.cs @@ -0,0 +1,156 @@ +using System; +using System.Diagnostics; +using Braintrust.Sdk.Config; +using Braintrust.Sdk.Trace; +using Xunit; + +namespace Braintrust.Sdk.Tests.Trace; + +public class BraintrustSpanProcessorTest : IDisposable +{ + private readonly ActivitySource _activitySource; + private readonly ActivityListener _activityListener; + + public BraintrustSpanProcessorTest() + { + _activitySource = new ActivitySource("test-source"); + + // Set up activity listener so activities are actually created + _activityListener = new ActivityListener + { + ShouldListenTo = source => source.Name == "test-source", + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData + }; + ActivitySource.AddActivityListener(_activityListener); + } + + public void Dispose() + { + _activityListener?.Dispose(); + _activitySource?.Dispose(); + } + + [Fact] + public void OnStart_AddsParentFromContext() + { + var config = BraintrustConfig.Of( + "BRAINTRUST_API_KEY", "test-key" + ); + var processor = new BraintrustSpanProcessor(config); + + var context = BraintrustContext.OfProject("proj-123"); + using (context.MakeCurrent()) + using (var activity = _activitySource.StartActivity("test-span")) + { + Assert.NotNull(activity); + processor.OnStart(activity); + + var parent = activity.GetTagItem(BraintrustSpanProcessor.ParentAttributeKey); + Assert.Equal("project_id:proj-123", parent); + } + } + + [Fact] + public void OnStart_AddsExperimentParentFromContext() + { + var config = BraintrustConfig.Of( + "BRAINTRUST_API_KEY", "test-key" + ); + var processor = new BraintrustSpanProcessor(config); + + var context = BraintrustContext.OfExperiment("exp-456"); + using (context.MakeCurrent()) + using (var activity = _activitySource.StartActivity("test-span")) + { + Assert.NotNull(activity); + processor.OnStart(activity); + + var parent = activity.GetTagItem(BraintrustSpanProcessor.ParentAttributeKey); + Assert.Equal("experiment_id:exp-456", parent); + } + } + + [Fact] + public void OnStart_AddsParentFromConfig() + { + var config = BraintrustConfig.Of( + "BRAINTRUST_API_KEY", "test-key", + "BRAINTRUST_DEFAULT_PROJECT_ID", "proj-config" + ); + var processor = new BraintrustSpanProcessor(config); + + using (var activity = _activitySource.StartActivity("test-span")) + { + Assert.NotNull(activity); + processor.OnStart(activity); + + var parent = activity.GetTagItem(BraintrustSpanProcessor.ParentAttributeKey); + Assert.Equal("project_id:proj-config", parent); + } + } + + [Fact] + public void OnStart_ContextOverridesConfig() + { + var config = BraintrustConfig.Of( + "BRAINTRUST_API_KEY", "test-key", + "BRAINTRUST_DEFAULT_PROJECT_ID", "proj-config" + ); + var processor = new BraintrustSpanProcessor(config); + + var context = BraintrustContext.OfProject("proj-context"); + using (context.MakeCurrent()) + using (var activity = _activitySource.StartActivity("test-span")) + { + Assert.NotNull(activity); + processor.OnStart(activity); + + var parent = activity.GetTagItem(BraintrustSpanProcessor.ParentAttributeKey); + Assert.Equal("project_id:proj-context", parent); + } + } + + [Fact] + public void OnStart_DoesNotOverrideExistingParent() + { + var config = BraintrustConfig.Of( + "BRAINTRUST_API_KEY", "test-key", + "BRAINTRUST_DEFAULT_PROJECT_ID", "proj-config" + ); + var processor = new BraintrustSpanProcessor(config); + + using (var activity = _activitySource.StartActivity("test-span")) + { + Assert.NotNull(activity); + // Set parent before processor runs + activity.SetTag(BraintrustSpanProcessor.ParentAttributeKey, "project_id:existing"); + + processor.OnStart(activity); + + var parent = activity.GetTagItem(BraintrustSpanProcessor.ParentAttributeKey); + Assert.Equal("project_id:existing", parent); + } + } + + [Fact] + public void OnStart_NoParent_WhenNoConfigOrContext() + { + var config = BraintrustConfig.Of( + "BRAINTRUST_API_KEY", "test-key", + "BRAINTRUST_DEFAULT_PROJECT_ID", "", + "BRAINTRUST_DEFAULT_PROJECT_NAME", "", + "BRAINTRUST_DEFAULT_EXPERIMENT_ID", "", + "BRAINTRUST_DEFAULT_EXPERIMENT_NAME", "" + ); + var processor = new BraintrustSpanProcessor(config); + + using (var activity = _activitySource.StartActivity("test-span")) + { + Assert.NotNull(activity); + processor.OnStart(activity); + + var parent = activity.GetTagItem(BraintrustSpanProcessor.ParentAttributeKey); + Assert.Null(parent); + } + } +} diff --git a/tests/Braintrust.Sdk.Tests/Trace/BraintrustTracingTest.cs b/tests/Braintrust.Sdk.Tests/Trace/BraintrustTracingTest.cs new file mode 100644 index 0000000..97538e9 --- /dev/null +++ b/tests/Braintrust.Sdk.Tests/Trace/BraintrustTracingTest.cs @@ -0,0 +1,48 @@ +using System; +using Braintrust.Sdk.Config; +using Braintrust.Sdk.Trace; +using Xunit; + +namespace Braintrust.Sdk.Tests.Trace; + +public class BraintrustTracingTest +{ + [Fact] + public void Of_CreatesTracerProvider() + { + var config = BraintrustConfig.Of( + "BRAINTRUST_API_KEY", "test-key", + "BRAINTRUST_API_URL", "https://test-api.example.com" + ); + + using var tracerProvider = BraintrustTracing.Of(config, registerGlobal: false); + + Assert.NotNull(tracerProvider); + } + + [Fact] + public void GetActivitySource_ReturnsActivitySource() + { + var activitySource = BraintrustTracing.GetActivitySource(); + + Assert.NotNull(activitySource); + Assert.Equal("braintrust-dotnet", activitySource.Name); + } + + [Fact] + public void Quickstart_CreatesTracerProvider() + { + // Set required environment variables + Environment.SetEnvironmentVariable("BRAINTRUST_API_KEY", "test-key"); + + try + { + using var tracerProvider = BraintrustTracing.Quickstart(); + Assert.NotNull(tracerProvider); + } + finally + { + Environment.SetEnvironmentVariable("BRAINTRUST_API_KEY", null); + } + } +}