Skip to content

Commit 95cd6d1

Browse files
author
Andrei Bratu
committed
singleton tracer + investigate non hl spans
1 parent ea16dfe commit 95cd6d1

4 files changed

Lines changed: 636 additions & 84 deletions

File tree

src/humanloop.client.ts

Lines changed: 142 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
1+
import {
2+
Instrumentation,
3+
registerInstrumentations,
4+
} from "@opentelemetry/instrumentation";
5+
import { Resource } from "@opentelemetry/resources";
16
import { NodeTracerProvider, Tracer } from "@opentelemetry/sdk-trace-node";
27
import { AnthropicInstrumentation } from "@traceloop/instrumentation-anthropic";
38
import { CohereInstrumentation } from "@traceloop/instrumentation-cohere";
49
import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai";
5-
import { Evaluators } from "api/resources/evaluators/client/Client";
610

711
import { HumanloopClient as BaseHumanloopClient } from "./Client";
812
import { ChatMessage } from "./api";
913
import { Evaluations as BaseEvaluations } from "./api/resources/evaluations/client/Client";
14+
import { Evaluators } from "./api/resources/evaluators/client/Client";
1015
import { Flows } from "./api/resources/flows/client/Client";
1116
import { Prompts } from "./api/resources/prompts/client/Client";
1217
import { Tools } from "./api/resources/tools/client/Client";
1318
import { ToolKernelRequest } from "./api/types/ToolKernelRequest";
1419
import { flowUtilityFactory } from "./decorators/flow";
1520
import { promptDecoratorFactory } from "./decorators/prompt";
1621
import { toolUtilityFactory } from "./decorators/tool";
22+
import { HumanloopEnvironment } from "./environments";
1723
import { HumanloopRuntimeError } from "./error";
1824
import { runEval } from "./evals/run";
1925
import {
@@ -26,6 +32,7 @@ import {
2632
import { HumanloopSpanExporter } from "./otel/exporter";
2733
import { HumanloopSpanProcessor } from "./otel/processor";
2834
import { overloadCall, overloadLog } from "./overload";
35+
import { SDK_VERSION } from "./version";
2936

3037
const RED = "\x1b[91m";
3138
const RESET = "\x1b[0m";
@@ -125,25 +132,123 @@ class ExtendedEvaluations extends BaseEvaluations {
125132
}
126133
}
127134

135+
class HumanloopTracerSingleton {
136+
private static instance: HumanloopTracerSingleton;
137+
private readonly tracerProvider: NodeTracerProvider;
138+
public readonly tracer: Tracer;
139+
140+
private constructor(config: {
141+
hlClientApiKey: string;
142+
hlClientBaseUrl: string;
143+
instrumentProviders?: {
144+
OpenAI?: any;
145+
Anthropic?: any;
146+
CohereAI?: any;
147+
};
148+
}) {
149+
this.tracerProvider = new NodeTracerProvider({
150+
resource: new Resource({
151+
attributes: {
152+
// @ts-ignore
153+
"service.name": "humanloop-typescript-sdk",
154+
"service.version": SDK_VERSION,
155+
},
156+
}),
157+
spanProcessors: [
158+
new HumanloopSpanProcessor(
159+
new HumanloopSpanExporter({
160+
hlClientHeaders: {
161+
"X-API-KEY": config.hlClientApiKey,
162+
"X-Fern-Language": "Typescript",
163+
"X-Fern-SDK-Name": "humanloop",
164+
"X-Fern-SDK-Version": SDK_VERSION,
165+
},
166+
hlClientBaseUrl: config.hlClientBaseUrl,
167+
}),
168+
),
169+
],
170+
});
171+
const instrumentations: Instrumentation[] = [];
172+
if (config.instrumentProviders?.OpenAI) {
173+
const openaiInstrumentation = new OpenAIInstrumentation({
174+
enrichTokens: true,
175+
});
176+
openaiInstrumentation.manuallyInstrument(config.instrumentProviders.OpenAI);
177+
openaiInstrumentation.setTracerProvider(this.tracerProvider);
178+
openaiInstrumentation.enable();
179+
instrumentations.push(openaiInstrumentation);
180+
}
181+
if (config.instrumentProviders?.Anthropic) {
182+
const anthropicInstrumentation = new AnthropicInstrumentation();
183+
anthropicInstrumentation.manuallyInstrument(
184+
config.instrumentProviders.Anthropic,
185+
);
186+
anthropicInstrumentation.setTracerProvider(this.tracerProvider);
187+
anthropicInstrumentation.enable();
188+
instrumentations.push(anthropicInstrumentation);
189+
}
190+
if (config.instrumentProviders?.CohereAI) {
191+
const cohereInstrumentation = new CohereInstrumentation();
192+
cohereInstrumentation.manuallyInstrument(
193+
config.instrumentProviders.CohereAI,
194+
);
195+
cohereInstrumentation.setTracerProvider(this.tracerProvider);
196+
cohereInstrumentation.enable();
197+
instrumentations.push(cohereInstrumentation);
198+
}
199+
200+
this.tracerProvider.register();
201+
202+
registerInstrumentations({
203+
tracerProvider: this.tracerProvider,
204+
instrumentations,
205+
});
206+
207+
this.tracer = this.tracerProvider.getTracer("humanloop.sdk");
208+
}
209+
210+
public static getInstance(config: {
211+
hlClientApiKey: string;
212+
hlClientBaseUrl: string;
213+
instrumentProviders?: {
214+
OpenAI?: any;
215+
Anthropic?: any;
216+
CohereAI?: any;
217+
};
218+
}): HumanloopTracerSingleton {
219+
if (!HumanloopTracerSingleton.instance) {
220+
HumanloopTracerSingleton.instance = new HumanloopTracerSingleton(config);
221+
}
222+
return HumanloopTracerSingleton.instance;
223+
}
224+
}
225+
128226
export class HumanloopClient extends BaseHumanloopClient {
129227
protected readonly _evaluations: ExtendedEvaluations;
130228
protected readonly _prompts_overloaded: Prompts;
131229
protected readonly _flows_overloaded: Flows;
132230
protected readonly _tools_overloaded: Tools;
133231
protected readonly _evaluators_overloaded: Evaluators;
134-
135-
protected readonly OpenAI?: any;
136-
protected readonly Anthropic?: any;
137-
protected readonly CohereAI?: any;
138-
139-
protected readonly opentelemetryTracerProvider: NodeTracerProvider;
140-
protected readonly opentelemetryTracer: Tracer;
232+
protected readonly instrumentProviders: {
233+
OpenAI?: any;
234+
Anthropic?: any;
235+
CohereAI?: any;
236+
};
237+
238+
protected get opentelemetryTracer(): Tracer {
239+
return HumanloopTracerSingleton.getInstance({
240+
hlClientApiKey: this.options().apiKey!.toString(),
241+
hlClientBaseUrl:
242+
this.options().baseUrl!.toString() || HumanloopEnvironment.Default,
243+
instrumentProviders: this.instrumentProviders,
244+
}).tracer;
245+
}
141246

142247
/**
143248
* Constructs a new instance of the Humanloop client.
144249
*
145250
* @param _options - The base options for the Humanloop client.
146-
* @param providerModules - LLM provider modules to instrument. Allows prompt decorator to spy on LLM provider calls and log them to Humanloop.
251+
* @param _options.instrumentProviders - LLM provider modules to instrument. Allows the prompt decorator to spy on provider calls and log them to Humanloop
147252
*
148253
* Pass LLM provider modules as such:
149254
*
@@ -152,27 +257,27 @@ export class HumanloopClient extends BaseHumanloopClient {
152257
* import { Anthropic } from "anthropic";
153258
* import { HumanloopClient } from "humanloop";
154259
*
155-
* const humanloop = new HumanloopClient({apiKey: process.env.HUMANLOOP_KEY}, { OpenAI, Anthropic });
260+
* const humanloop = new HumanloopClient({
261+
* apiKey: process.env.HUMANLOOP_KEY,
262+
* instrumentProviders: { OpenAI, Anthropic },
263+
* });
156264
*
157265
* const openai = new OpenAI({apiKey: process.env.OPENAI_KEY});
158266
* const anthropic = new Anthropic({apiKey: process.env.ANTHROPIC_KEY});
159267
* ```
160268
*/
161269
constructor(
162-
_options: BaseHumanloopClient.Options,
163-
providers?: {
164-
OpenAI?: any;
165-
Anthropic?: any;
166-
CohereAI?: any;
270+
_options: BaseHumanloopClient.Options & {
271+
instrumentProviders?: {
272+
OpenAI?: any;
273+
Anthropic?: any;
274+
CohereAI?: any;
275+
};
167276
},
168277
) {
169278
super(_options);
170279

171-
const { OpenAI, Anthropic, CohereAI } = providers ?? {};
172-
173-
this.OpenAI = OpenAI;
174-
this.Anthropic = Anthropic;
175-
this.CohereAI = CohereAI;
280+
this.instrumentProviders = _options.instrumentProviders || {};
176281

177282
this._prompts_overloaded = overloadLog(super.prompts);
178283
this._prompts_overloaded = overloadCall(this._prompts_overloaded);
@@ -185,60 +290,31 @@ export class HumanloopClient extends BaseHumanloopClient {
185290

186291
this._evaluations = new ExtendedEvaluations(_options, this);
187292

188-
this.opentelemetryTracerProvider = new NodeTracerProvider({
189-
spanProcessors: [
190-
new HumanloopSpanProcessor(new HumanloopSpanExporter(this)),
191-
],
293+
// Initialize the tracer singleton
294+
HumanloopTracerSingleton.getInstance({
295+
hlClientApiKey: this.options().apiKey!.toString(),
296+
hlClientBaseUrl:
297+
this.options().baseUrl!.toString() || HumanloopEnvironment.Default,
298+
instrumentProviders: this.instrumentProviders,
192299
});
193-
194-
if (OpenAI) {
195-
const instrumentor = new OpenAIInstrumentation({
196-
enrichTokens: true,
197-
});
198-
instrumentor.manuallyInstrument(OpenAI);
199-
instrumentor.setTracerProvider(this.opentelemetryTracerProvider);
200-
instrumentor.enable();
201-
}
202-
203-
if (Anthropic) {
204-
const instrumentor = new AnthropicInstrumentation();
205-
instrumentor.manuallyInstrument(Anthropic);
206-
instrumentor.setTracerProvider(this.opentelemetryTracerProvider);
207-
instrumentor.enable();
208-
}
209-
210-
if (CohereAI) {
211-
const instrumentor = new CohereInstrumentation();
212-
instrumentor.manuallyInstrument(CohereAI);
213-
instrumentor.setTracerProvider(this.opentelemetryTracerProvider);
214-
instrumentor.enable();
215-
}
216-
217-
this.opentelemetryTracerProvider.register();
218-
219-
this.opentelemetryTracer =
220-
this.opentelemetryTracerProvider.getTracer("humanloop.sdk");
221300
}
222301

223302
public options(): BaseHumanloopClient.Options {
224303
return this._options;
225304
}
226305

227306
// Check if user has passed the LLM provider instrumentors
228-
private assertProviders(func: Function) {
229-
const noProviderInstrumented = [
230-
this.OpenAI,
231-
this.Anthropic,
232-
this.CohereAI,
233-
].every((p) => !p);
234-
if (noProviderInstrumented) {
307+
private assertProviders() {
308+
const userDidNotPassProviders = Object.values(this.instrumentProviders).every(
309+
(provider) => !provider,
310+
);
311+
if (userDidNotPassProviders) {
235312
throw new HumanloopRuntimeError(
236313
`${RED}To use the @prompt decorator, pass your LLM client library into the Humanloop client constructor. For example:\n\n
237314
import { OpenAI } from "openai";
238315
import { HumanloopClient } from "humanloop";
239316
240317
const humanloop = new HumanloopClient({apiKey: process.env.HUMANLOOP_KEY}, { OpenAI });
241-
// Create the the OpenAI client after the client is initialized
242318
const openai = new OpenAI();
243319
${RESET}`,
244320
);
@@ -251,9 +327,13 @@ ${RESET}`,
251327
*
252328
* ```typescript
253329
* import { OpenAI } from "openai";
330+
* import { Anthropic } from "anthropic";
254331
* import { HumanloopClient } from "humanloop";
255332
*
256-
* const humanloop = new HumanloopClient({apiKey: process.env.HUMANLOOP_KEY}, { OpenAI });
333+
* const humanloop = new HumanloopClient({
334+
* apiKey: process.env.HUMANLOOP_KEY,
335+
* instrumentProviders: { OpenAI, Anthropic },
336+
* });
257337
* const openai = new OpenAI({apiKey: process.env.OPENAI_KEY});
258338
*
259339
* const callOpenaiWithHumanloop = humanloop.prompt({
@@ -350,7 +430,7 @@ ${RESET}`,
350430
: () => O extends Promise<infer R>
351431
? Promise<R | undefined>
352432
: Promise<O | undefined> {
353-
this.assertProviders(args.callable);
433+
this.assertProviders();
354434
// @ts-ignore
355435
return promptDecoratorFactory(args.path, args.callable);
356436
}

src/otel/exporter.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
33

44
import { HumanloopRuntimeError } from "../error";
55
import { getEvaluationContext } from "../evals";
6-
import { HumanloopClient } from "../humanloop.client";
7-
import { SDK_VERSION } from "../version";
86
import {
97
HUMANLOOP_FILE_TYPE_KEY,
108
HUMANLOOP_LOG_KEY,
@@ -18,12 +16,20 @@ import {
1816
import { TracesData } from "./proto/trace";
1917

2018
export class HumanloopSpanExporter implements SpanExporter {
21-
private readonly client: HumanloopClient;
19+
private readonly hlClientBaseUrl: string;
20+
21+
private readonly hlClientHeaders: Record<string, string>;
22+
2223
private shutdownFlag: boolean;
24+
2325
private readonly uploadPromises: Promise<void>[];
2426

25-
constructor(client: HumanloopClient) {
26-
this.client = client;
27+
constructor(options: {
28+
hlClientHeaders: Record<string, string>;
29+
hlClientBaseUrl: string;
30+
}) {
31+
this.hlClientHeaders = options.hlClientHeaders;
32+
this.hlClientBaseUrl = options.hlClientBaseUrl;
2733
this.shutdownFlag = false;
2834
this.uploadPromises = [];
2935
}
@@ -42,7 +48,21 @@ export class HumanloopSpanExporter implements SpanExporter {
4248
for (const span of spans) {
4349
const fileType = span.attributes[HUMANLOOP_FILE_TYPE_KEY];
4450
if (!fileType) {
45-
throw new Error("Internal error: Span does not have type set");
51+
return {
52+
code: ExportResultCode.FAILED,
53+
error: new Error(
54+
`Internal error: Span does not have type set:\n${JSON.stringify(
55+
{
56+
attributes: span.attributes,
57+
name: span.name,
58+
kind: span.kind,
59+
instrumentationLibrary: span.instrumentationLibrary,
60+
},
61+
null,
62+
2,
63+
)}`,
64+
),
65+
};
4666
}
4767

4868
let logArgs = {};
@@ -87,20 +107,11 @@ export class HumanloopSpanExporter implements SpanExporter {
87107
span: ReadableSpan,
88108
evalContextCallback: ((log_id: string) => Promise<void>) | null,
89109
): Promise<void> {
90-
const response = await fetch(
91-
`${this.client.options().baseUrl}/import/otel/v1/traces`,
92-
{
93-
method: "POST",
94-
headers: {
95-
"X-API-KEY": this.client.options().apiKey!.toString(),
96-
"X-Fern-Language": "Typescript",
97-
"X-Fern-SDK-Name": "humanloop",
98-
"X-Fern-SDK-Version": SDK_VERSION,
99-
},
100-
body: JSON.stringify(this.spanToPayload(span)),
101-
},
102-
);
103-
110+
const response = await fetch(`${this.hlClientBaseUrl}/import/otel/v1/traces`, {
111+
method: "POST",
112+
headers: this.hlClientHeaders,
113+
body: JSON.stringify(this.spanToPayload(span)),
114+
});
104115
if (response.status !== 200) {
105116
throw new HumanloopRuntimeError(
106117
`Failed to upload OTEL span to Humanloop: ${JSON.stringify(await response.json())} ${response.status}`,

0 commit comments

Comments
 (0)