braintrust 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -0
- data/lib/braintrust/config.rb +1 -0
- data/lib/braintrust/eval/runner.rb +4 -4
- data/lib/braintrust/internal/env.rb +81 -0
- data/lib/braintrust/trace/span_exporter.rb +7 -3
- data/lib/braintrust/trace/span_filter.rb +2 -1
- data/lib/braintrust/trace/span_origin.rb +105 -0
- data/lib/braintrust/trace/span_processor.rb +3 -1
- data/lib/braintrust/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1047e872fe3a0acf40117bd9a0fcbf4e5024edbe3c1eb3d3bd1e32f60e5ffb18
|
|
4
|
+
data.tar.gz: e4c412abf3cf748ff157248feb821724ba4ffb76b67aecd8d1735179572afa93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41f183ac2bb20f64bca5c576f0decabdea9b7a4bcb46cbcf0c66292eb34f7e75a3b91516cd5520b4a3e207669eff0fa212afd1f75c1d6b172a44ea27bbc91e4b
|
|
7
|
+
data.tar.gz: 8a98b9ea710b69ac0b4e22cbdbed2b55bfab0d25db5fda2afc872931ce27a4eb9183433db03be6cedcb295dfca2f1fa43583620d848c55e9fa8b19629ecbfb8e
|
data/README.md
CHANGED
data/lib/braintrust/config.rb
CHANGED
|
@@ -91,8 +91,8 @@ module Braintrust
|
|
|
91
91
|
# Set attributes known before task execution
|
|
92
92
|
eval_span.set_attribute("braintrust.parent", eval_context.parent_span_attr) if eval_context.parent_span_attr
|
|
93
93
|
set_json_attr(eval_span, "braintrust.span_attributes", build_span_attributes("eval"))
|
|
94
|
-
set_json_attr(eval_span, "braintrust.input_json",
|
|
95
|
-
set_json_attr(eval_span, "braintrust.
|
|
94
|
+
set_json_attr(eval_span, "braintrust.input_json", kase.input)
|
|
95
|
+
set_json_attr(eval_span, "braintrust.expected_json", kase.expected) unless kase.expected.nil?
|
|
96
96
|
set_json_attr(eval_span, "braintrust.metadata", kase.metadata) if kase.metadata
|
|
97
97
|
eval_span.set_attribute("braintrust.tags", kase.tags) if kase.tags
|
|
98
98
|
eval_span.set_attribute("braintrust.origin", kase.origin) if kase.origin
|
|
@@ -103,7 +103,6 @@ module Braintrust
|
|
|
103
103
|
rescue => e
|
|
104
104
|
# Error already recorded on task span, set eval span status
|
|
105
105
|
eval_span.status = OpenTelemetry::Trace::Status.error(e.message)
|
|
106
|
-
set_json_attr(eval_span, "braintrust.output_json", {output: nil})
|
|
107
106
|
errors << "Task failed for input '#{kase.input}': #{e.message}"
|
|
108
107
|
report_progress(eval_span, kase, error: e.message)
|
|
109
108
|
next
|
|
@@ -134,7 +133,7 @@ module Braintrust
|
|
|
134
133
|
end
|
|
135
134
|
|
|
136
135
|
# Set output after task completes
|
|
137
|
-
set_json_attr(eval_span, "braintrust.output_json",
|
|
136
|
+
set_json_attr(eval_span, "braintrust.output_json", kase.output)
|
|
138
137
|
|
|
139
138
|
report_progress(eval_span, kase, data: kase.output)
|
|
140
139
|
end
|
|
@@ -151,6 +150,7 @@ module Braintrust
|
|
|
151
150
|
task_span.set_attribute("braintrust.parent", eval_context.parent_span_attr) if eval_context.parent_span_attr
|
|
152
151
|
set_json_attr(task_span, "braintrust.span_attributes", build_span_attributes("task"))
|
|
153
152
|
set_json_attr(task_span, "braintrust.input_json", kase.input)
|
|
153
|
+
set_json_attr(task_span, "braintrust.expected_json", kase.expected) unless kase.expected.nil?
|
|
154
154
|
|
|
155
155
|
begin
|
|
156
156
|
output = eval_context.task.call(
|
|
@@ -5,6 +5,8 @@ module Braintrust
|
|
|
5
5
|
# Environment variable utilities.
|
|
6
6
|
module Env
|
|
7
7
|
ENV_AUTO_INSTRUMENT = "BRAINTRUST_AUTO_INSTRUMENT"
|
|
8
|
+
ENV_ENVIRONMENT_NAME = "BRAINTRUST_ENVIRONMENT_NAME"
|
|
9
|
+
ENV_ENVIRONMENT_TYPE = "BRAINTRUST_ENVIRONMENT_TYPE"
|
|
8
10
|
ENV_INSTRUMENT_EXCEPT = "BRAINTRUST_INSTRUMENT_EXCEPT"
|
|
9
11
|
ENV_INSTRUMENT_ONLY = "BRAINTRUST_INSTRUMENT_ONLY"
|
|
10
12
|
ENV_FLUSH_ON_EXIT = "BRAINTRUST_FLUSH_ON_EXIT"
|
|
@@ -26,6 +28,36 @@ module Braintrust
|
|
|
26
28
|
parse_list(ENV_INSTRUMENT_ONLY)
|
|
27
29
|
end
|
|
28
30
|
|
|
31
|
+
def self.detect_environment
|
|
32
|
+
env_type = env_value(ENV_ENVIRONMENT_TYPE)
|
|
33
|
+
env_name = env_value(ENV_ENVIRONMENT_NAME)
|
|
34
|
+
if present?(env_type) || present?(env_name)
|
|
35
|
+
return {type: env_type, name: env_name}.compact
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
"GITHUB_ACTIONS" => "github_actions",
|
|
40
|
+
"GITLAB_CI" => "gitlab_ci",
|
|
41
|
+
"CIRCLECI" => "circleci",
|
|
42
|
+
"BUILDKITE" => "buildkite",
|
|
43
|
+
"JENKINS_URL" => "jenkins",
|
|
44
|
+
"JENKINS_HOME" => "jenkins",
|
|
45
|
+
"TF_BUILD" => "azure_pipelines",
|
|
46
|
+
"TEAMCITY_VERSION" => "teamcity",
|
|
47
|
+
"TRAVIS" => "travis",
|
|
48
|
+
"BITBUCKET_BUILD_NUMBER" => "bitbucket"
|
|
49
|
+
}.each do |key, name|
|
|
50
|
+
return {type: "ci", name: name} if present?(ENV[key])
|
|
51
|
+
end
|
|
52
|
+
return {type: "ci", name: "ci"} if present?(ENV["CI"])
|
|
53
|
+
|
|
54
|
+
server_name = detect_server_environment_name
|
|
55
|
+
return {type: "server", name: server_name} if server_name
|
|
56
|
+
|
|
57
|
+
deployment_mode_environment(ENV["RAILS_ENV"]) ||
|
|
58
|
+
deployment_mode_environment(ENV["RACK_ENV"])
|
|
59
|
+
end
|
|
60
|
+
|
|
29
61
|
# Parse a comma-separated environment variable into an array of symbols.
|
|
30
62
|
# @param key [String] The environment variable name
|
|
31
63
|
# @return [Array<Symbol>, nil] Array of symbols, or nil if not set
|
|
@@ -34,6 +66,55 @@ module Braintrust
|
|
|
34
66
|
return nil unless value
|
|
35
67
|
value.split(",").map(&:strip).map(&:to_sym)
|
|
36
68
|
end
|
|
69
|
+
|
|
70
|
+
def self.deployment_mode_environment(value)
|
|
71
|
+
return nil unless present?(value)
|
|
72
|
+
|
|
73
|
+
normalized = value.strip.downcase
|
|
74
|
+
return {type: "server", name: normalized} if ["production", "staging"].include?(normalized)
|
|
75
|
+
return {type: "local", name: normalized} if ["development", "local"].include?(normalized)
|
|
76
|
+
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
private_class_method :deployment_mode_environment
|
|
80
|
+
|
|
81
|
+
def self.detect_server_environment_name
|
|
82
|
+
{"VERCEL" => "vercel", "NETLIFY" => "netlify"}.each do |key, name|
|
|
83
|
+
return name if present?(ENV[key])
|
|
84
|
+
end
|
|
85
|
+
return "ecs" if present?(ENV["ECS_CONTAINER_METADATA_URI"]) || present?(ENV["ECS_CONTAINER_METADATA_URI_V4"])
|
|
86
|
+
|
|
87
|
+
aws_execution_env = env_value("AWS_EXECUTION_ENV")
|
|
88
|
+
return "ecs" if aws_execution_env&.start_with?("AWS_ECS_")
|
|
89
|
+
return "aws_lambda" if aws_execution_env&.start_with?("AWS_Lambda_")
|
|
90
|
+
return "aws_lambda" if present?(ENV["AWS_LAMBDA_FUNCTION_NAME"])
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
"K_SERVICE" => "cloud_run",
|
|
94
|
+
"FUNCTION_TARGET" => "gcp_functions",
|
|
95
|
+
"KUBERNETES_SERVICE_HOST" => "kubernetes",
|
|
96
|
+
"DYNO" => "heroku",
|
|
97
|
+
"FLY_APP_NAME" => "fly",
|
|
98
|
+
"RAILWAY_ENVIRONMENT" => "railway",
|
|
99
|
+
"RENDER_SERVICE_NAME" => "render"
|
|
100
|
+
}.each do |key, name|
|
|
101
|
+
return name if present?(ENV[key])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
private_class_method :detect_server_environment_name
|
|
107
|
+
|
|
108
|
+
def self.env_value(key)
|
|
109
|
+
value = ENV[key]
|
|
110
|
+
value&.strip unless value.nil? || value.strip.empty?
|
|
111
|
+
end
|
|
112
|
+
private_class_method :env_value
|
|
113
|
+
|
|
114
|
+
def self.present?(value)
|
|
115
|
+
!value.nil? && !value.strip.empty?
|
|
116
|
+
end
|
|
117
|
+
private_class_method :present?
|
|
37
118
|
end
|
|
38
119
|
end
|
|
39
120
|
end
|
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
require "opentelemetry/exporter/otlp"
|
|
4
4
|
require_relative "../state"
|
|
5
|
+
require_relative "span_origin"
|
|
5
6
|
|
|
6
7
|
module Braintrust
|
|
7
8
|
module Trace
|
|
8
|
-
# Custom OTLP exporter
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# Custom OTLP exporter for the Braintrust backend. On export it:
|
|
10
|
+
# - stamps span origin provenance onto each SpanData (via the prepended SpanOrigin behavior)
|
|
11
|
+
# - groups spans by braintrust.parent and sets the x-bt-parent header per group,
|
|
12
|
+
# so the backend routes them to the correct experiment/project
|
|
11
13
|
#
|
|
12
14
|
# Thread safety: BatchSpanProcessor serializes export() calls via its
|
|
13
15
|
# @export_mutex, so @headers mutation here is safe.
|
|
14
16
|
class SpanExporter < OpenTelemetry::Exporter::OTLP::Exporter
|
|
17
|
+
prepend SpanOrigin
|
|
18
|
+
|
|
15
19
|
PARENT_ATTR_KEY = SpanProcessor::PARENT_ATTR_KEY
|
|
16
20
|
PARENT_HEADER = "x-bt-parent"
|
|
17
21
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../version"
|
|
5
|
+
require_relative "../internal/env"
|
|
6
|
+
|
|
7
|
+
module Braintrust
|
|
8
|
+
module Trace
|
|
9
|
+
# Span origin provenance decoration.
|
|
10
|
+
#
|
|
11
|
+
# This is a *behavior*, not a type. Prepend it onto any exporter whose
|
|
12
|
+
# +export(span_data, timeout:)+ it can +super+ into, and every exported
|
|
13
|
+
# SpanData gains a +braintrust.context_json+ attribute carrying span origin
|
|
14
|
+
# (SDK name/version, instrumentation scope, environment).
|
|
15
|
+
#
|
|
16
|
+
# Because it only ever touches the SpanData copies handed to *this*
|
|
17
|
+
# exporter, the enrichment is invisible to any other exporter sharing the
|
|
18
|
+
# same tracer provider - there is no global patch and nothing leaks onto a
|
|
19
|
+
# customer's other OTel traces.
|
|
20
|
+
module SpanOrigin
|
|
21
|
+
CONTEXT_JSON_ATTR_KEY = "braintrust.context_json"
|
|
22
|
+
|
|
23
|
+
# Exporter behavior: enrich each SpanData with span origin before export.
|
|
24
|
+
# @param span_data [Array<OpenTelemetry::SDK::Trace::SpanData>]
|
|
25
|
+
# @return [Integer] export result from the wrapped exporter
|
|
26
|
+
def export(span_data, timeout: nil)
|
|
27
|
+
# Environment is process-global and stable; read it once per batch
|
|
28
|
+
# rather than once per span. It is cheap (ENV reads only).
|
|
29
|
+
environment = Internal::Env.detect_environment
|
|
30
|
+
enriched = span_data.map { |sd| SpanOrigin.enrich(sd, environment: environment) }
|
|
31
|
+
super(enriched, timeout: timeout)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Enrich a single SpanData with span origin provenance.
|
|
35
|
+
# Mutates the SpanData in place (replacing its frozen attributes hash with
|
|
36
|
+
# a new frozen hash - it never mutates the shared hash) and returns it.
|
|
37
|
+
# @param span_data [OpenTelemetry::SDK::Trace::SpanData]
|
|
38
|
+
# @param environment [Hash, nil] process environment ({type:, name:}) or nil
|
|
39
|
+
# @return [OpenTelemetry::SDK::Trace::SpanData]
|
|
40
|
+
def self.enrich(span_data, environment:)
|
|
41
|
+
attributes = span_data.attributes || {}
|
|
42
|
+
enriched_attributes = attributes_with_origin(
|
|
43
|
+
attributes,
|
|
44
|
+
instrumentation_name: instrumentation_name(span_data),
|
|
45
|
+
environment: environment
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return span_data if enriched_attributes.equal?(attributes)
|
|
49
|
+
|
|
50
|
+
span_data.attributes = enriched_attributes.freeze
|
|
51
|
+
span_data.total_recorded_attributes = enriched_attributes.length
|
|
52
|
+
span_data
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.attributes_with_origin(attributes, instrumentation_name:, environment:)
|
|
56
|
+
context = parse_context_json(attributes[CONTEXT_JSON_ATTR_KEY])
|
|
57
|
+
span_origin = context["span_origin"].is_a?(Hash) ? context["span_origin"] : {}
|
|
58
|
+
|
|
59
|
+
span_origin_changed = false
|
|
60
|
+
unless span_origin.key?("name")
|
|
61
|
+
span_origin["name"] = "braintrust.sdk.ruby"
|
|
62
|
+
span_origin_changed = true
|
|
63
|
+
end
|
|
64
|
+
unless span_origin.key?("version")
|
|
65
|
+
span_origin["version"] = Braintrust::VERSION
|
|
66
|
+
span_origin_changed = true
|
|
67
|
+
end
|
|
68
|
+
unless span_origin.key?("instrumentation")
|
|
69
|
+
span_origin["instrumentation"] = {"name" => instrumentation_name}
|
|
70
|
+
span_origin_changed = true
|
|
71
|
+
end
|
|
72
|
+
if environment && !span_origin.key?("environment")
|
|
73
|
+
span_origin["environment"] = environment
|
|
74
|
+
span_origin_changed = true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
context_changed = context["span_origin"] != span_origin || span_origin_changed
|
|
78
|
+
return attributes unless context_changed
|
|
79
|
+
|
|
80
|
+
context["span_origin"] = span_origin
|
|
81
|
+
attributes.merge(CONTEXT_JSON_ATTR_KEY => JSON.generate(context))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.parse_context_json(raw)
|
|
85
|
+
return {} unless raw.is_a?(String) && !raw.strip.empty?
|
|
86
|
+
|
|
87
|
+
parsed = JSON.parse(raw)
|
|
88
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
89
|
+
rescue JSON::ParserError
|
|
90
|
+
{}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.instrumentation_name(span)
|
|
94
|
+
if span.respond_to?(:instrumentation_scope) && span.instrumentation_scope&.respond_to?(:name)
|
|
95
|
+
return span.instrumentation_scope.name
|
|
96
|
+
end
|
|
97
|
+
if span.respond_to?(:instrumentation_library) && span.instrumentation_library&.respond_to?(:name)
|
|
98
|
+
return span.instrumentation_library.name
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
"braintrust-ruby"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -83,9 +83,11 @@ module Braintrust
|
|
|
83
83
|
# If no filters, keep everything
|
|
84
84
|
return true if @filters.empty?
|
|
85
85
|
|
|
86
|
+
span_data = span.respond_to?(:to_span_data) ? span.to_span_data : span
|
|
87
|
+
|
|
86
88
|
# Apply filters in order - first non-zero result wins
|
|
87
89
|
@filters.each do |filter|
|
|
88
|
-
result = filter.call(
|
|
90
|
+
result = filter.call(span_data)
|
|
89
91
|
return true if result > 0 # Keep span
|
|
90
92
|
return false if result < 0 # Drop span
|
|
91
93
|
# result == 0: no influence, continue to next filter
|
data/lib/braintrust/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: braintrust
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Braintrust
|
|
@@ -182,6 +182,7 @@ files:
|
|
|
182
182
|
- lib/braintrust/trace/attachment.rb
|
|
183
183
|
- lib/braintrust/trace/span_exporter.rb
|
|
184
184
|
- lib/braintrust/trace/span_filter.rb
|
|
185
|
+
- lib/braintrust/trace/span_origin.rb
|
|
185
186
|
- lib/braintrust/trace/span_processor.rb
|
|
186
187
|
- lib/braintrust/vendor/mustache.rb
|
|
187
188
|
- lib/braintrust/vendor/mustache/context.rb
|