Skip to content
Draft
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
57 changes: 57 additions & 0 deletions integrations/otel-js/src/otel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ interface BraintrustSpanProcessorOptions {
* Additional headers to send with telemetry data
*/
headers?: Record<string, string>;
environment?: { type: string; name?: string };
/**
* @internal
* Internal option for dependency injection during testing.
Expand All @@ -200,6 +201,47 @@ interface BraintrustSpanProcessorOptions {
_spanProcessor?: SpanProcessor;
}

const SDK_VERSION = "3.20.0";

function spanOriginContext(environment?: { type: string; name?: string }) {
return {
span_origin: {
name: "braintrust.sdk.javascript",
version: SDK_VERSION,
instrumentation: { name: "braintrust-otel-js" },
...(environment ? { environment } : {}),
},
};
}

function detectEnvironment(
explicit?: { type: string; name?: string },
): { type: string; name?: string } | undefined {
if (explicit) return explicit;
const envType = process.env.BRAINTRUST_ENVIRONMENT_TYPE;
if (envType) {
const envName = process.env.BRAINTRUST_ENVIRONMENT_NAME;
return envName ? { type: envType, name: envName } : { type: envType };
}
if (process.env.GITHUB_ACTIONS) return { type: "ci", name: "github_actions" };
if (process.env.GITLAB_CI) return { type: "ci", name: "gitlab_ci" };
if (process.env.CIRCLECI) return { type: "ci", name: "circleci" };
if (process.env.BUILDKITE) return { type: "ci", name: "buildkite" };
if (process.env.CI) return { type: "ci", name: "ci" };
if (process.env.VERCEL) return { type: "server", name: "vercel" };
if (process.env.NETLIFY) return { type: "server", name: "netlify" };
if (process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.AWS_EXECUTION_ENV) {
return { type: "server", name: "aws_lambda" };
}
if (process.env.NODE_ENV === "production" || process.env.NODE_ENV === "staging") {
return { type: "server", name: process.env.NODE_ENV };
}
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "local") {
return { type: "local", name: process.env.NODE_ENV };
}
return undefined;
}

class LazyBraintrustOTLPTraceExporter implements SpanExporter {
private readonly diagLogger = diag.createComponentLogger({
namespace: "@braintrust/otel",
Expand Down Expand Up @@ -377,8 +419,10 @@ export class BraintrustSpanProcessor implements SpanProcessor {
private readonly processor: SpanProcessor;
private readonly aiSpanProcessor: SpanProcessor;
private readonly exporter?: LazyBraintrustOTLPTraceExporter;
private readonly environment?: { type: string; name?: string };

constructor(options: BraintrustSpanProcessorOptions = {}) {
this.environment = detectEnvironment(options.environment);
// If a processor is injected (for testing), use it directly
if (options._spanProcessor) {
this.processor = options._spanProcessor;
Expand Down Expand Up @@ -496,6 +540,19 @@ export class BraintrustSpanProcessor implements SpanProcessor {
span.setAttributes?.({ "braintrust.parent": parentValue });
}
}

const contextJson = JSON.stringify(spanOriginContext(this.environment));
span.setAttributes?.({
"braintrust.context_json": contextJson,
...(this.environment
? {
"braintrust.environment.type": this.environment.type,
...(this.environment.name
? { "braintrust.environment.name": this.environment.name }
: {}),
}
: {}),
});
} catch {
// If there's an exception, just don't set braintrust.parent
}
Expand Down
16 changes: 15 additions & 1 deletion js/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ import { lintTemplate as _lintMustacheTemplate } from "./template/mustache-utils
import { prettifyXact } from "../util/index";
import { SpanCache, CachedSpan } from "./span-cache";
import type { EvalParameters, InferParameters } from "./eval-parameters";
import {
detectSpanOriginEnvironment,
mergeSpanOriginContext,
type SpanOriginEnvironment,
} from "./span-origin";

// Manual type definition for inline attachments (not in generated_types)
const InlineAttachmentReferenceSchema = z.object({
Expand Down Expand Up @@ -677,6 +682,7 @@ export class BraintrustState {
private _idGenerator: IDGenerator | null = null;
private _contextManager: ContextManager | null = null;
private _otelFlushCallback: (() => Promise<void>) | null = null;
public spanOriginEnvironment: SpanOriginEnvironment | undefined;

constructor(private loginParams: LoginOptions) {
this.id = `${new Date().toLocaleString()}-${stateNonce++}`; // This is for debugging. uuidv4() breaks on platforms like Cloudflare.
Expand Down Expand Up @@ -736,6 +742,7 @@ export class BraintrustState {
});

this.spanCache = new SpanCache({ disabled: loginParams.disableSpanCache });
this.spanOriginEnvironment = detectSpanOriginEnvironment();
}

public resetLoginInfo() {
Expand Down Expand Up @@ -4398,6 +4405,7 @@ type AsyncFlushArg<IsAsyncFlush> = {
export type InitLoggerOptions<IsAsyncFlush> = FullLoginOptions & {
projectName?: string;
projectId?: string;
environment?: SpanOriginEnvironment;
setCurrent?: boolean;
state?: BraintrustState;
orgProjectMetadata?: OrgProjectMetadata;
Expand Down Expand Up @@ -4431,6 +4439,7 @@ export function initLogger<IsAsyncFlush extends boolean = true>(
orgName,
forceLogin,
debugLogLevel,
environment,
fetch,
state: stateArg,
} = options || {};
Expand All @@ -4452,6 +4461,7 @@ export function initLogger<IsAsyncFlush extends boolean = true>(

const state = stateArg ?? _globalState;
state.setDebugLogLevel(debugLogLevel);
state.spanOriginEnvironment = detectSpanOriginEnvironment(environment);

// Enable queue size limit enforcement for initLogger() calls
// This ensures production observability doesn't OOM customer processes
Expand Down Expand Up @@ -6854,7 +6864,11 @@ export class SpanImpl implements Span {
metrics: {
start: args.startTime ?? getCurrentUnixTimestamp(),
},
context: { ...callerLocation },
context: mergeSpanOriginContext(
{ ...callerLocation },
"braintrust-js-logger",
this._state.spanOriginEnvironment,
),
span_attributes: {
name,
type,
Expand Down
40 changes: 39 additions & 1 deletion js/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,17 @@ export function configureNode() {
iso.getPastNAncestors = getPastNAncestors;
iso.getEnv = (name) => {
const value = process.env[name];
return name === "BRAINTRUST_API_KEY" && !value?.trim() ? undefined : value;
if (name === "BRAINTRUST_API_KEY") {
return value?.trim() ? value : undefined;
}
if (
(name === "BRAINTRUST_ENVIRONMENT_TYPE" ||
name === "BRAINTRUST_ENVIRONMENT_NAME") &&
!value?.trim()
) {
return getNearestBraintrustEnvValue(name);
}
return value;
};
iso.getBraintrustApiKey = async () => {
const value = process.env.BRAINTRUST_API_KEY;
Expand Down Expand Up @@ -152,3 +162,31 @@ export function configureNode() {
// Enable auto-instrumentation
registry.enable();
}

function getNearestBraintrustEnvValue(name: string): string | undefined {
for (
let dir = process.cwd(), depth = 0;
depth <= BRAINTRUST_ENV_SEARCH_PARENT_LIMIT;
dir = path.dirname(dir), depth++
) {
const envPath = path.join(dir, ".env.braintrust");
try {
const parsed = dotenv.parse(fsSync.readFileSync(envPath, "utf8"));
const value = parsed[name];
return value?.trim() ? value : undefined;
} catch (e) {
if (
typeof e !== "object" ||
e === null ||
!("code" in e) ||
e.code !== "ENOENT"
) {
return undefined;
}
}
if (path.dirname(dir) === dir) {
break;
}
}
return undefined;
}
111 changes: 111 additions & 0 deletions js/src/span-origin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import iso from "./isomorph";

export type SpanOriginEnvironment = {
type: string;
name?: string;
};

export type SpanOrigin = {
name: string;
version: string;
instrumentation: { name: string };
environment?: SpanOriginEnvironment;
};

const SDK_VERSION = "3.20.0";

export function detectSpanOriginEnvironment(
explicit?: SpanOriginEnvironment,
): SpanOriginEnvironment | undefined {
if (explicit) return explicit;

const envType = iso.getEnv("BRAINTRUST_ENVIRONMENT_TYPE");
if (envType) {
const envName = iso.getEnv("BRAINTRUST_ENVIRONMENT_NAME");
return envName ? { type: envType, name: envName } : { type: envType };
}

const ci = firstPresent([
["GITHUB_ACTIONS", "github_actions"],
["GITLAB_CI", "gitlab_ci"],
["CIRCLECI", "circleci"],
["BUILDKITE", "buildkite"],
["JENKINS_URL", "jenkins"],
["JENKINS_HOME", "jenkins"],
["TF_BUILD", "azure_pipelines"],
["TEAMCITY_VERSION", "teamcity"],
["TRAVIS", "travis"],
["BITBUCKET_BUILD_NUMBER", "bitbucket"],
]);
if (ci) return { type: "ci", name: ci };
if (iso.getEnv("CI")) return { type: "ci", name: "ci" };

const server = firstPresent([
["VERCEL", "vercel"],
["NETLIFY", "netlify"],
["AWS_LAMBDA_FUNCTION_NAME", "aws_lambda"],
["AWS_EXECUTION_ENV", "aws_lambda"],
["K_SERVICE", "cloud_run"],
["FUNCTION_TARGET", "gcp_functions"],
["KUBERNETES_SERVICE_HOST", "kubernetes"],
["ECS_CONTAINER_METADATA_URI", "ecs"],
["ECS_CONTAINER_METADATA_URI_V4", "ecs"],
["DYNO", "heroku"],
["FLY_APP_NAME", "fly"],
["RAILWAY_ENVIRONMENT", "railway"],
["RENDER_SERVICE_NAME", "render"],
]);
if (server) return { type: "server", name: server };

return deploymentModeEnvironment("NODE_ENV", iso.getEnv("NODE_ENV"));
}

export function makeSpanOrigin(
instrumentationName: string,
environment?: SpanOriginEnvironment,
): SpanOrigin {
return {
name: "braintrust.sdk.javascript",
version: SDK_VERSION,
instrumentation: { name: instrumentationName },
...(environment ? { environment } : {}),
};
}

export function mergeSpanOriginContext(
context: Record<string, unknown> | undefined,
instrumentationName: string,
environment?: SpanOriginEnvironment,
): Record<string, unknown> {
const next = { ...(context ?? {}) };
const current =
isObject(next.span_origin) ? { ...next.span_origin } : {};
next.span_origin = {
...makeSpanOrigin(instrumentationName, environment),
...current,
};
return next;
}

function firstPresent(entries: Array<[string, string]>): string | undefined {
return entries.find(([key]) => Boolean(iso.getEnv(key)))?.[1];
}

function deploymentModeEnvironment(
_key: string,
value: string | undefined,
): SpanOriginEnvironment | undefined {
if (!value) return undefined;
const normalized = value.toLowerCase();
if (normalized === "production" || normalized === "staging") {
return { type: "server", name: normalized };
}
if (normalized === "development" || normalized === "local") {
return { type: "local", name: normalized };
}
return undefined;
}

function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
Loading