@lmnr-ai/lmnr 0.4.6 → 0.4.7-alpha
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.
- package/README.md +1 -1
- package/package.json +37 -3
- package/src/decorators.ts +3 -6
- package/src/laminar.ts +33 -2
- package/src/sdk/configuration/index.ts +68 -0
- package/src/sdk/errors/index.ts +57 -0
- package/src/sdk/interfaces/index.ts +1 -0
- package/src/sdk/interfaces/initialize-options.interface.ts +114 -0
- package/src/sdk/node-server-sdk.ts +7 -0
- package/src/sdk/telemetry/telemetry.ts +86 -0
- package/src/sdk/tracing/association.ts +21 -0
- package/src/sdk/tracing/decorators.ts +140 -0
- package/src/sdk/tracing/index.ts +357 -0
- package/src/sdk/tracing/manual.ts +179 -0
- package/src/sdk/tracing/sampler.ts +35 -0
- package/src/sdk/tracing/tracing.ts +19 -0
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ This will automatically instrument most of the LLM, Vector DB, and related
|
|
|
24
24
|
calls with OpenTelemetry-compatible instrumentation.
|
|
25
25
|
|
|
26
26
|
We rely on the amazing [OpenLLMetry](https://github.com/traceloop/openllmetry), open-source package
|
|
27
|
-
by TraceLoop, to achieve that.
|
|
27
|
+
by TraceLoop, to achieve that. Also this package uses Traceloop's [Openllmetry JS library](https://github.com/traceloop/openllmetry-js).
|
|
28
28
|
|
|
29
29
|
## Instrumentation
|
|
30
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmnr-ai/lmnr",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7-alpha",
|
|
4
4
|
"description": "TypeScript SDK for Laminar AI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -21,18 +21,52 @@
|
|
|
21
21
|
},
|
|
22
22
|
"homepage": "https://github.com/lmnr-ai/lmnr-ts#README",
|
|
23
23
|
"devDependencies": {
|
|
24
|
+
"@anthropic-ai/sdk": "^0.27.3",
|
|
25
|
+
"@aws-sdk/client-bedrock-runtime": "^3.649.0",
|
|
26
|
+
"@azure/openai": "2.0.0-beta.2",
|
|
27
|
+
"@google-cloud/aiplatform": "^3.29.0",
|
|
28
|
+
"@google-cloud/vertexai": "^1.7.0",
|
|
29
|
+
"@langchain/core": "^0.2.32",
|
|
30
|
+
"@pinecone-database/pinecone": "^3.0.2",
|
|
31
|
+
"@qdrant/js-client-rest": "^1.11.0",
|
|
24
32
|
"@types/node": "^22.5.4",
|
|
25
33
|
"@types/uuid": "^10.0.0",
|
|
26
34
|
"bufferutil": "^4.0.8",
|
|
35
|
+
"chromadb": "^1.8.1",
|
|
36
|
+
"cohere-ai": "^7.13.0",
|
|
37
|
+
"langchain": "^0.2.18",
|
|
38
|
+
"llamaindex": "^0.5.23",
|
|
39
|
+
"openai": "^4.58.2",
|
|
40
|
+
"runnables": "link:@langchain/core/runnables",
|
|
27
41
|
"tsup": "^8.2.4",
|
|
28
|
-
"typescript": "^5.5.4"
|
|
42
|
+
"typescript": "^5.5.4",
|
|
43
|
+
"vectorstores": "link:@langchain/core/vectorstores"
|
|
29
44
|
},
|
|
30
45
|
"dependencies": {
|
|
31
46
|
"@grpc/grpc-js": "^1.11.2",
|
|
32
47
|
"@opentelemetry/api": "^1.9.0",
|
|
48
|
+
"@opentelemetry/core": "^1.26.0",
|
|
33
49
|
"@opentelemetry/exporter-trace-otlp-grpc": "^0.53.0",
|
|
50
|
+
"@opentelemetry/exporter-trace-otlp-proto": "^0.53.0",
|
|
51
|
+
"@opentelemetry/instrumentation": "^0.53.0",
|
|
52
|
+
"@opentelemetry/resources": "^1.26.0",
|
|
53
|
+
"@opentelemetry/sdk-node": "^0.53.0",
|
|
54
|
+
"@opentelemetry/sdk-trace-base": "^1.26.0",
|
|
55
|
+
"@opentelemetry/sdk-trace-node": "^1.26.0",
|
|
56
|
+
"@opentelemetry/semantic-conventions": "^1.27.0",
|
|
34
57
|
"@traceloop/ai-semantic-conventions": "^0.11.0",
|
|
35
|
-
"@traceloop/
|
|
58
|
+
"@traceloop/instrumentation-anthropic": "^0.11.1",
|
|
59
|
+
"@traceloop/instrumentation-azure": "^0.11.1",
|
|
60
|
+
"@traceloop/instrumentation-bedrock": "^0.11.1",
|
|
61
|
+
"@traceloop/instrumentation-chromadb": "^0.11.1",
|
|
62
|
+
"@traceloop/instrumentation-cohere": "^0.11.1",
|
|
63
|
+
"@traceloop/instrumentation-langchain": "^0.11.1",
|
|
64
|
+
"@traceloop/instrumentation-llamaindex": "^0.11.1",
|
|
65
|
+
"@traceloop/instrumentation-openai": "^0.11.1",
|
|
66
|
+
"@traceloop/instrumentation-pinecone": "^0.11.1",
|
|
67
|
+
"@traceloop/instrumentation-qdrant": "^0.11.1",
|
|
68
|
+
"@traceloop/instrumentation-vertexai": "^0.11.1",
|
|
69
|
+
"posthog-node": "^4.2.0",
|
|
36
70
|
"uuid": "^10.0.0"
|
|
37
71
|
},
|
|
38
72
|
"scripts": {
|
package/src/decorators.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Laminar } from './laminar';
|
|
1
|
+
import { withEntity } from './sdk/node-server-sdk'
|
|
3
2
|
|
|
4
3
|
interface ObserveOptions {
|
|
5
4
|
name?: string;
|
|
@@ -32,9 +31,6 @@ export async function observe<A extends unknown[], F extends (...args: A) => Ret
|
|
|
32
31
|
userId,
|
|
33
32
|
}: ObserveOptions, fn: F, ...args: A): Promise<ReturnType<F>> {
|
|
34
33
|
|
|
35
|
-
if (!Laminar.initialized()) {
|
|
36
|
-
throw new Error('Laminar not initialized. Please call Laminar.initialize(projectApiKey) before using observe.');
|
|
37
|
-
};
|
|
38
34
|
let associationProperties = {};
|
|
39
35
|
if (sessionId) {
|
|
40
36
|
associationProperties = { ...associationProperties, "session_id": sessionId };
|
|
@@ -42,5 +38,6 @@ export async function observe<A extends unknown[], F extends (...args: A) => Ret
|
|
|
42
38
|
if (userId) {
|
|
43
39
|
associationProperties = { ...associationProperties, "user_id": userId };
|
|
44
40
|
}
|
|
45
|
-
|
|
41
|
+
|
|
42
|
+
return await withEntity<A, F>({ name: name ?? fn.name, associationProperties }, fn, undefined, ...args);
|
|
46
43
|
}
|
package/src/laminar.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { PipelineRunResponse, PipelineRunRequest, EvaluationDatapoint, EvaluationStatus } from './types';
|
|
2
2
|
import { Attributes, AttributeValue, context, createContextKey, isSpanContextValid, TimeInput, trace } from '@opentelemetry/api';
|
|
3
|
-
import { InitializeOptions, initialize as traceloopInitialize } from '
|
|
3
|
+
import { InitializeOptions, initialize as traceloopInitialize } from './sdk/node-server-sdk'
|
|
4
4
|
import { otelSpanIdToUUID, otelTraceIdToUUID } from './utils';
|
|
5
5
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
|
|
6
6
|
import { Metadata } from '@grpc/grpc-js';
|
|
7
|
+
import { ASSOCIATION_PROPERTIES_KEY } from './sdk/tracing/tracing';
|
|
7
8
|
|
|
8
9
|
// quick patch to get the traceloop's default tracer, since their
|
|
9
10
|
// `getTracer` function is not exported.
|
|
@@ -37,6 +38,26 @@ export class Laminar {
|
|
|
37
38
|
* @param baseUrl - Url of Laminar endpoint, or the custom open telemetry ingester.
|
|
38
39
|
* If not specified, defaults to https://api.lmnr.ai:8443. For locally hosted Laminar,
|
|
39
40
|
* default setting must be http://localhost:8001.
|
|
41
|
+
* @param instrumentModules - List of modules to instrument.
|
|
42
|
+
* If not specified, all auto-instrumentable modules will be instrumented, which include
|
|
43
|
+
* LLM calls (OpenAI, Anthropic, etc), Langchain, VectorDB calls (Pinecone, Qdrant, etc).
|
|
44
|
+
* Pass an empty object {} to disable any kind of automatic instrumentation.
|
|
45
|
+
* If you only want to auto-instrument specific modules, then pass them in the object.
|
|
46
|
+
*
|
|
47
|
+
* Example:
|
|
48
|
+
* ```typescript
|
|
49
|
+
* import { Laminar as L } from '@lmnr-ai/lmnr';
|
|
50
|
+
* import { OpenAI } from 'openai';
|
|
51
|
+
* import * as ChainsModule from "langchain/chains";
|
|
52
|
+
*
|
|
53
|
+
* // Initialize Laminar while auto-instrumenting Langchain and OpenAI modules.
|
|
54
|
+
* L.initialize({ projectApiKey: "<LMNR_PROJECT_API_KEY>", instrumentModules: {
|
|
55
|
+
* langchain: {
|
|
56
|
+
* chainsModule: ChainsModule
|
|
57
|
+
* },
|
|
58
|
+
* openAI: OpenAI
|
|
59
|
+
* } });
|
|
60
|
+
* ```
|
|
40
61
|
*
|
|
41
62
|
* @throws {Error} - If project API key is not set
|
|
42
63
|
*/
|
|
@@ -276,7 +297,17 @@ export class Laminar {
|
|
|
276
297
|
if (userId) {
|
|
277
298
|
associationProperties = { ...associationProperties, "user_id": userId };
|
|
278
299
|
}
|
|
279
|
-
|
|
300
|
+
|
|
301
|
+
let entityContext = context.active();
|
|
302
|
+
const currentAssociationProperties = entityContext.getValue(ASSOCIATION_PROPERTIES_KEY);
|
|
303
|
+
if (associationProperties) {
|
|
304
|
+
entityContext = entityContext.setValue(
|
|
305
|
+
ASSOCIATION_PROPERTIES_KEY,
|
|
306
|
+
{ ...(currentAssociationProperties ?? {}), ...associationProperties },
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return entityContext;
|
|
280
311
|
}
|
|
281
312
|
|
|
282
313
|
public static async createEvaluation(name: string) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { InitializeOptions } from "../interfaces";
|
|
2
|
+
import { startTracing } from "../tracing";
|
|
3
|
+
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
|
4
|
+
import { InitializationError } from "../errors";
|
|
5
|
+
|
|
6
|
+
export let _configuration: InitializeOptions | undefined;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Initializes the SDK.
|
|
10
|
+
* Must be called once before any other SDK methods.
|
|
11
|
+
*
|
|
12
|
+
* @param options - The options to initialize the SDK. See the {@link InitializeOptions} for details.
|
|
13
|
+
* @throws {InitializationError} if the configuration is invalid or if failed to fetch feature data.
|
|
14
|
+
*/
|
|
15
|
+
export const initialize = (options: InitializeOptions) => {
|
|
16
|
+
if (_configuration) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!options.baseUrl) {
|
|
21
|
+
options.baseUrl =
|
|
22
|
+
process.env.LMNR_BASE_URL || "https://api.lmnr.ai:8443";
|
|
23
|
+
}
|
|
24
|
+
if (!options.apiKey) {
|
|
25
|
+
options.apiKey = process.env.LMNR_PROJECT_API_KEY;
|
|
26
|
+
}
|
|
27
|
+
if (options.apiKey && typeof options.apiKey !== "string") {
|
|
28
|
+
throw new InitializationError('"apiKey" must be a string');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!options.appName) {
|
|
32
|
+
options.appName = process.env.npm_package_name;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_configuration = Object.freeze(options);
|
|
36
|
+
|
|
37
|
+
if (options.logLevel) {
|
|
38
|
+
diag.setLogger(
|
|
39
|
+
new DiagConsoleLogger(),
|
|
40
|
+
logLevelToOtelLogLevel(options.logLevel),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!options.silenceInitializationMessage) {
|
|
45
|
+
console.log(
|
|
46
|
+
`Laminar exporting traces to ${
|
|
47
|
+
_configuration.exporter ? "a custom exporter" : _configuration.baseUrl
|
|
48
|
+
}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
startTracing(_configuration);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const logLevelToOtelLogLevel = (
|
|
56
|
+
logLevel: "debug" | "info" | "warn" | "error",
|
|
57
|
+
) => {
|
|
58
|
+
switch (logLevel) {
|
|
59
|
+
case "debug":
|
|
60
|
+
return DiagLogLevel.DEBUG;
|
|
61
|
+
case "info":
|
|
62
|
+
return DiagLogLevel.INFO;
|
|
63
|
+
case "warn":
|
|
64
|
+
return DiagLogLevel.WARN;
|
|
65
|
+
case "error":
|
|
66
|
+
return DiagLogLevel.ERROR;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The severity of an error.
|
|
3
|
+
*/
|
|
4
|
+
export const SEVERITY = {
|
|
5
|
+
Warning: "Warning",
|
|
6
|
+
Error: "Error",
|
|
7
|
+
Critical: "Critical",
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export type Severity = (typeof SEVERITY)[keyof typeof SEVERITY];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base class for all Traceloop errors.
|
|
14
|
+
*/
|
|
15
|
+
export class TraceloopError extends Error {
|
|
16
|
+
/**
|
|
17
|
+
* The severity of the error.
|
|
18
|
+
*/
|
|
19
|
+
severity: Severity;
|
|
20
|
+
/**
|
|
21
|
+
* The underlying cause of the error.
|
|
22
|
+
*/
|
|
23
|
+
underlyingCause?: Error;
|
|
24
|
+
|
|
25
|
+
constructor(message: string, severity: Severity = SEVERITY.Error) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.severity = severity;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class NotInitializedError extends TraceloopError {
|
|
32
|
+
constructor() {
|
|
33
|
+
super(
|
|
34
|
+
`The Traceloop SDK must be initialized by calling the "initialize" function prior to use.`,
|
|
35
|
+
SEVERITY.Critical,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class InitializationError extends TraceloopError {
|
|
41
|
+
constructor(message?: string, cause?: Error) {
|
|
42
|
+
super(message ?? "Failed to initialize Traceloop SDK", SEVERITY.Critical);
|
|
43
|
+
this.underlyingCause = cause;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class ArgumentNotProvidedError extends TraceloopError {
|
|
48
|
+
constructor(argumentName: string) {
|
|
49
|
+
super(`The "${argumentName}" argument is required and must be a string.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class PromptNotFoundError extends TraceloopError {
|
|
54
|
+
constructor(key: string) {
|
|
55
|
+
super(`The prompt "${key}" was not found in the registry.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./initialize-options.interface";
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { SpanExporter, SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
2
|
+
import { TextMapPropagator, ContextManager } from "@opentelemetry/api";
|
|
3
|
+
import type * as openai from "openai";
|
|
4
|
+
import type * as anthropic from "@anthropic-ai/sdk";
|
|
5
|
+
import type * as azure from "@azure/openai";
|
|
6
|
+
import type * as cohere from "cohere-ai";
|
|
7
|
+
import type * as bedrock from "@aws-sdk/client-bedrock-runtime";
|
|
8
|
+
import type * as aiplatform from "@google-cloud/aiplatform";
|
|
9
|
+
import type * as vertexAI from "@google-cloud/vertexai";
|
|
10
|
+
import type * as pinecone from "@pinecone-database/pinecone";
|
|
11
|
+
import type * as ChainsModule from "langchain/chains";
|
|
12
|
+
import type * as AgentsModule from "langchain/agents";
|
|
13
|
+
import type * as ToolsModule from "langchain/tools";
|
|
14
|
+
import type * as RunnableModule from "@langchain/core/runnables";
|
|
15
|
+
import type * as VectorStoreModule from "@langchain/core/vectorstores";
|
|
16
|
+
import type * as llamaindex from "llamaindex";
|
|
17
|
+
import type * as chromadb from "chromadb";
|
|
18
|
+
import type * as qdrant from "@qdrant/js-client-rest";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options for initializing the Traceloop SDK.
|
|
22
|
+
*/
|
|
23
|
+
export interface InitializeOptions {
|
|
24
|
+
/**
|
|
25
|
+
* The app name to be used when reporting traces. Optional.
|
|
26
|
+
* Defaults to the package name.
|
|
27
|
+
*/
|
|
28
|
+
appName?: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The API Key for sending traces data. Optional.
|
|
32
|
+
* Defaults to the TRACELOOP_API_KEY environment variable.
|
|
33
|
+
*/
|
|
34
|
+
apiKey?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The OTLP endpoint for sending traces data. Optional.
|
|
38
|
+
* Defaults to TRACELOOP_BASE_URL environment variable or https://api.traceloop.com/
|
|
39
|
+
*/
|
|
40
|
+
baseUrl?: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sends traces and spans without batching, for local developement. Optional.
|
|
44
|
+
* Defaults to false.
|
|
45
|
+
*/
|
|
46
|
+
disableBatch?: boolean;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Defines default log level for SDK and all instrumentations. Optional.
|
|
50
|
+
* Defaults to error.
|
|
51
|
+
*/
|
|
52
|
+
logLevel?: "debug" | "info" | "warn" | "error";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether to log prompts, completions and embeddings on traces. Optional.
|
|
56
|
+
* Defaults to true.
|
|
57
|
+
*/
|
|
58
|
+
traceContent?: boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The OpenTelemetry SpanExporter to be used for sending traces data. Optional.
|
|
62
|
+
* Defaults to the OTLP exporter.
|
|
63
|
+
*/
|
|
64
|
+
exporter?: SpanExporter;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The OpenTelemetry SpanProcessor to be used for processing traces data. Optional.
|
|
68
|
+
* Defaults to the BatchSpanProcessor.
|
|
69
|
+
*/
|
|
70
|
+
processor?: SpanProcessor;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The OpenTelemetry Propagator to use. Optional.
|
|
74
|
+
* Defaults to OpenTelemetry SDK defaults.
|
|
75
|
+
*/
|
|
76
|
+
propagator?: TextMapPropagator;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The OpenTelemetry ContextManager to use. Optional.
|
|
80
|
+
* Defaults to OpenTelemetry SDK defaults.
|
|
81
|
+
*/
|
|
82
|
+
contextManager?: ContextManager;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Explicitly specify modules to instrument. Optional.
|
|
86
|
+
* This is a workaround specific to Next.js, see https://www.traceloop.com/docs/openllmetry/getting-started-nextjs
|
|
87
|
+
*/
|
|
88
|
+
instrumentModules?: {
|
|
89
|
+
openAI?: typeof openai.OpenAI;
|
|
90
|
+
anthropic?: typeof anthropic;
|
|
91
|
+
azureOpenAI?: typeof azure;
|
|
92
|
+
cohere?: typeof cohere;
|
|
93
|
+
bedrock?: typeof bedrock;
|
|
94
|
+
google_vertexai?: typeof vertexAI;
|
|
95
|
+
google_aiplatform?: typeof aiplatform;
|
|
96
|
+
pinecone?: typeof pinecone;
|
|
97
|
+
langchain?: {
|
|
98
|
+
chainsModule?: typeof ChainsModule;
|
|
99
|
+
agentsModule?: typeof AgentsModule;
|
|
100
|
+
toolsModule?: typeof ToolsModule;
|
|
101
|
+
runnablesModule?: typeof RunnableModule;
|
|
102
|
+
vectorStoreModule?: typeof VectorStoreModule;
|
|
103
|
+
};
|
|
104
|
+
llamaIndex?: typeof llamaindex;
|
|
105
|
+
chromadb?: typeof chromadb;
|
|
106
|
+
qdrant?: typeof qdrant;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether to silence the initialization message. Optional.
|
|
111
|
+
* Defaults to false.
|
|
112
|
+
*/
|
|
113
|
+
silenceInitializationMessage?: boolean;
|
|
114
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./errors";
|
|
2
|
+
export { InitializeOptions } from "./interfaces";
|
|
3
|
+
export { initialize } from "./configuration";
|
|
4
|
+
export { forceFlush } from "./tracing";
|
|
5
|
+
export * from "./tracing/decorators";
|
|
6
|
+
export * from "./tracing/manual";
|
|
7
|
+
export * from "./tracing/association";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as os from "os";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { v4 as uuid } from "uuid";
|
|
5
|
+
import { PostHog } from "posthog-node";
|
|
6
|
+
const { version } = require('../../../package.json');
|
|
7
|
+
|
|
8
|
+
export class Telemetry {
|
|
9
|
+
private static instance: Telemetry;
|
|
10
|
+
|
|
11
|
+
private static readonly ANON_ID_PATH = `${os.homedir()}/.cache/lmnr/telemetry_anon_id`;
|
|
12
|
+
private static readonly UNKNOWN_ANON_ID = "UNKNOWN";
|
|
13
|
+
|
|
14
|
+
private telemetryEnabled: boolean;
|
|
15
|
+
private posthog: PostHog | undefined;
|
|
16
|
+
private anonId: string | undefined;
|
|
17
|
+
|
|
18
|
+
public static getInstance(): Telemetry {
|
|
19
|
+
if (!Telemetry.instance) {
|
|
20
|
+
Telemetry.instance = new Telemetry();
|
|
21
|
+
}
|
|
22
|
+
return Telemetry.instance;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private constructor() {
|
|
26
|
+
this.telemetryEnabled =
|
|
27
|
+
!process.env.LMNR_TELEMETRY ||
|
|
28
|
+
process.env.LMNR_TELEMETRY.toLowerCase() === "true";
|
|
29
|
+
|
|
30
|
+
if (this.telemetryEnabled) {
|
|
31
|
+
this.posthog = new PostHog(
|
|
32
|
+
"phc_dUMdjfNKf11jcHgtn7juSnT4P1pO0tafsPUWt4PuwG7",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private getAnonId() {
|
|
38
|
+
if (this.anonId) {
|
|
39
|
+
return this.anonId;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
if (!fs.existsSync(Telemetry.ANON_ID_PATH)) {
|
|
44
|
+
fs.mkdirSync(path.dirname(Telemetry.ANON_ID_PATH), { recursive: true });
|
|
45
|
+
const anonIdFile = fs.openSync(Telemetry.ANON_ID_PATH, "w");
|
|
46
|
+
this.anonId = uuid();
|
|
47
|
+
fs.writeSync(anonIdFile, this.anonId);
|
|
48
|
+
fs.closeSync(anonIdFile);
|
|
49
|
+
} else {
|
|
50
|
+
const anonIdFile = fs.openSync(Telemetry.ANON_ID_PATH, "r");
|
|
51
|
+
this.anonId = fs.readFileSync(anonIdFile, "utf8");
|
|
52
|
+
fs.closeSync(anonIdFile);
|
|
53
|
+
}
|
|
54
|
+
return this.anonId;
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return Telemetry.UNKNOWN_ANON_ID;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private getContext() {
|
|
61
|
+
return {
|
|
62
|
+
sdk: "typescript",
|
|
63
|
+
sdk_version: version,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public capture(event: string, properties?: Record<string, string>) {
|
|
68
|
+
if (this.telemetryEnabled && this.posthog) {
|
|
69
|
+
this.posthog.capture({
|
|
70
|
+
distinctId: this.getAnonId(),
|
|
71
|
+
event,
|
|
72
|
+
properties: {
|
|
73
|
+
...properties,
|
|
74
|
+
...this.getContext(),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
this.posthog.flush();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public logException(error: Error) {
|
|
82
|
+
if (this.telemetryEnabled) {
|
|
83
|
+
this.capture("error", { error: error.message, stack: error.stack || "" });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { context } from "@opentelemetry/api";
|
|
2
|
+
import { ASSOCIATION_PROPERTIES_KEY } from "./tracing";
|
|
3
|
+
|
|
4
|
+
export function withAssociationProperties<
|
|
5
|
+
A extends unknown[],
|
|
6
|
+
F extends (...args: A) => ReturnType<F>,
|
|
7
|
+
>(
|
|
8
|
+
properties: { [name: string]: string },
|
|
9
|
+
fn: F,
|
|
10
|
+
thisArg?: ThisParameterType<F>,
|
|
11
|
+
...args: A
|
|
12
|
+
) {
|
|
13
|
+
if (Object.keys(properties).length === 0) {
|
|
14
|
+
return fn.apply(thisArg, args);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const newContext = context
|
|
18
|
+
.active()
|
|
19
|
+
.setValue(ASSOCIATION_PROPERTIES_KEY, properties);
|
|
20
|
+
return context.with(newContext, fn, thisArg, ...args);
|
|
21
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Span, context } from "@opentelemetry/api";
|
|
2
|
+
import { suppressTracing } from "@opentelemetry/core";
|
|
3
|
+
import {
|
|
4
|
+
ASSOCIATION_PROPERTIES_KEY,
|
|
5
|
+
getTracer,
|
|
6
|
+
} from "./tracing";
|
|
7
|
+
import {
|
|
8
|
+
SpanAttributes,
|
|
9
|
+
} from "@traceloop/ai-semantic-conventions";
|
|
10
|
+
import { shouldSendTraces } from ".";
|
|
11
|
+
import { Telemetry } from "../telemetry/telemetry";
|
|
12
|
+
|
|
13
|
+
export type DecoratorConfig = {
|
|
14
|
+
name: string;
|
|
15
|
+
associationProperties?: { [name: string]: string };
|
|
16
|
+
inputParameters?: unknown[];
|
|
17
|
+
suppressTracing?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function withEntity<
|
|
21
|
+
A extends unknown[],
|
|
22
|
+
F extends (...args: A) => ReturnType<F>,
|
|
23
|
+
>(
|
|
24
|
+
{
|
|
25
|
+
name,
|
|
26
|
+
associationProperties,
|
|
27
|
+
inputParameters,
|
|
28
|
+
suppressTracing: shouldSuppressTracing,
|
|
29
|
+
}: DecoratorConfig,
|
|
30
|
+
fn: F,
|
|
31
|
+
thisArg?: ThisParameterType<F>,
|
|
32
|
+
...args: A
|
|
33
|
+
) {
|
|
34
|
+
let entityContext = context.active();
|
|
35
|
+
|
|
36
|
+
const currentAssociationProperties = entityContext.getValue(ASSOCIATION_PROPERTIES_KEY);
|
|
37
|
+
|
|
38
|
+
if (associationProperties) {
|
|
39
|
+
entityContext = entityContext.setValue(
|
|
40
|
+
ASSOCIATION_PROPERTIES_KEY,
|
|
41
|
+
{ ...(currentAssociationProperties ?? {}), ...associationProperties },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (shouldSuppressTracing) {
|
|
46
|
+
entityContext = suppressTracing(entityContext);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return context.with(entityContext, () =>
|
|
50
|
+
getTracer().startActiveSpan(
|
|
51
|
+
name,
|
|
52
|
+
{},
|
|
53
|
+
entityContext,
|
|
54
|
+
async (span: Span) => {
|
|
55
|
+
if (shouldSendTraces()) {
|
|
56
|
+
try {
|
|
57
|
+
const input = inputParameters ?? args;
|
|
58
|
+
if (
|
|
59
|
+
input.length === 1 &&
|
|
60
|
+
typeof input[0] === "object" &&
|
|
61
|
+
!(input[0] instanceof Map)
|
|
62
|
+
) {
|
|
63
|
+
span.setAttribute(
|
|
64
|
+
SpanAttributes.TRACELOOP_ENTITY_INPUT,
|
|
65
|
+
serialize({ args: [], kwargs: input[0] }),
|
|
66
|
+
);
|
|
67
|
+
} else {
|
|
68
|
+
span.setAttribute(
|
|
69
|
+
SpanAttributes.TRACELOOP_ENTITY_INPUT,
|
|
70
|
+
serialize({
|
|
71
|
+
args: input,
|
|
72
|
+
kwargs: {},
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
Telemetry.getInstance().logException(error as any);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const res = fn.apply(thisArg, args);
|
|
82
|
+
if (res instanceof Promise) {
|
|
83
|
+
return res.then((resolvedRes) => {
|
|
84
|
+
try {
|
|
85
|
+
if (shouldSendTraces()) {
|
|
86
|
+
span.setAttribute(
|
|
87
|
+
SpanAttributes.TRACELOOP_ENTITY_OUTPUT,
|
|
88
|
+
serialize(resolvedRes),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
Telemetry.getInstance().logException(error as any);
|
|
93
|
+
} finally {
|
|
94
|
+
span.end();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return resolvedRes;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
if (shouldSendTraces()) {
|
|
102
|
+
span.setAttribute(
|
|
103
|
+
SpanAttributes.TRACELOOP_ENTITY_OUTPUT,
|
|
104
|
+
serialize(res),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
Telemetry.getInstance().logException(error as any);
|
|
109
|
+
} finally {
|
|
110
|
+
span.end();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return res;
|
|
114
|
+
},
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function cleanInput(input: unknown): unknown {
|
|
120
|
+
if (input instanceof Map) {
|
|
121
|
+
return Array.from(input.entries());
|
|
122
|
+
} else if (Array.isArray(input)) {
|
|
123
|
+
return input.map((value) => cleanInput(value));
|
|
124
|
+
} else if (!input) {
|
|
125
|
+
return input;
|
|
126
|
+
} else if (typeof input === "object") {
|
|
127
|
+
// serialize object one by one
|
|
128
|
+
const output: any = {};
|
|
129
|
+
Object.entries(input as any).forEach(([key, value]) => {
|
|
130
|
+
output[key] = cleanInput(value);
|
|
131
|
+
});
|
|
132
|
+
return output;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return input;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function serialize(input: unknown): string {
|
|
139
|
+
return JSON.stringify(cleanInput(input));
|
|
140
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
3
|
+
import {
|
|
4
|
+
SimpleSpanProcessor,
|
|
5
|
+
BatchSpanProcessor,
|
|
6
|
+
SpanProcessor,
|
|
7
|
+
} from "@opentelemetry/sdk-trace-node";
|
|
8
|
+
import { baggageUtils } from "@opentelemetry/core";
|
|
9
|
+
import { Span, context, diag } from "@opentelemetry/api";
|
|
10
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
11
|
+
import { Resource } from "@opentelemetry/resources";
|
|
12
|
+
import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
13
|
+
import { Instrumentation } from "@opentelemetry/instrumentation";
|
|
14
|
+
import { InitializeOptions } from "../interfaces";
|
|
15
|
+
import {
|
|
16
|
+
Detector,
|
|
17
|
+
DetectorSync,
|
|
18
|
+
IResource,
|
|
19
|
+
ResourceDetectionConfig,
|
|
20
|
+
envDetectorSync,
|
|
21
|
+
hostDetectorSync,
|
|
22
|
+
processDetectorSync,
|
|
23
|
+
} from "@opentelemetry/resources"
|
|
24
|
+
import {
|
|
25
|
+
ASSOCIATION_PROPERTIES_KEY,
|
|
26
|
+
} from "./tracing";
|
|
27
|
+
import { Telemetry } from "../telemetry/telemetry";
|
|
28
|
+
import { _configuration } from "../configuration";
|
|
29
|
+
import {
|
|
30
|
+
SpanAttributes,
|
|
31
|
+
} from "@traceloop/ai-semantic-conventions";
|
|
32
|
+
import { AnthropicInstrumentation } from "@traceloop/instrumentation-anthropic";
|
|
33
|
+
import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai";
|
|
34
|
+
import { AzureOpenAIInstrumentation } from "@traceloop/instrumentation-azure";
|
|
35
|
+
import { LlamaIndexInstrumentation } from "@traceloop/instrumentation-llamaindex";
|
|
36
|
+
import {
|
|
37
|
+
AIPlatformInstrumentation,
|
|
38
|
+
VertexAIInstrumentation,
|
|
39
|
+
} from "@traceloop/instrumentation-vertexai";
|
|
40
|
+
import { BedrockInstrumentation } from "@traceloop/instrumentation-bedrock";
|
|
41
|
+
import { CohereInstrumentation } from "@traceloop/instrumentation-cohere";
|
|
42
|
+
import { PineconeInstrumentation } from "@traceloop/instrumentation-pinecone";
|
|
43
|
+
import { LangChainInstrumentation } from "@traceloop/instrumentation-langchain";
|
|
44
|
+
import { ChromaDBInstrumentation } from "@traceloop/instrumentation-chromadb";
|
|
45
|
+
import { QdrantInstrumentation } from "@traceloop/instrumentation-qdrant";
|
|
46
|
+
|
|
47
|
+
let _sdk: NodeSDK;
|
|
48
|
+
let _spanProcessor: SimpleSpanProcessor | BatchSpanProcessor;
|
|
49
|
+
let openAIInstrumentation: OpenAIInstrumentation | undefined;
|
|
50
|
+
let anthropicInstrumentation: AnthropicInstrumentation | undefined;
|
|
51
|
+
let azureOpenAIInstrumentation: AzureOpenAIInstrumentation | undefined;
|
|
52
|
+
let cohereInstrumentation: CohereInstrumentation | undefined;
|
|
53
|
+
let vertexaiInstrumentation: VertexAIInstrumentation | undefined;
|
|
54
|
+
let aiplatformInstrumentation: AIPlatformInstrumentation | undefined;
|
|
55
|
+
let bedrockInstrumentation: BedrockInstrumentation | undefined;
|
|
56
|
+
let langchainInstrumentation: LangChainInstrumentation | undefined;
|
|
57
|
+
let llamaIndexInstrumentation: LlamaIndexInstrumentation | undefined;
|
|
58
|
+
let pineconeInstrumentation: PineconeInstrumentation | undefined;
|
|
59
|
+
let chromadbInstrumentation: ChromaDBInstrumentation | undefined;
|
|
60
|
+
let qdrantInstrumentation: QdrantInstrumentation | undefined;
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
const instrumentations: Instrumentation[] = [];
|
|
64
|
+
|
|
65
|
+
const initInstrumentations = () => {
|
|
66
|
+
const exceptionLogger = (e: Error) => Telemetry.getInstance().logException(e);
|
|
67
|
+
const enrichTokens =
|
|
68
|
+
(process.env.TRACELOOP_ENRICH_TOKENS || "true").toLowerCase() === "true";
|
|
69
|
+
|
|
70
|
+
openAIInstrumentation = new OpenAIInstrumentation({
|
|
71
|
+
enrichTokens,
|
|
72
|
+
exceptionLogger,
|
|
73
|
+
});
|
|
74
|
+
instrumentations.push(openAIInstrumentation);
|
|
75
|
+
|
|
76
|
+
anthropicInstrumentation = new AnthropicInstrumentation({ exceptionLogger });
|
|
77
|
+
instrumentations.push(anthropicInstrumentation);
|
|
78
|
+
|
|
79
|
+
azureOpenAIInstrumentation = new AzureOpenAIInstrumentation({
|
|
80
|
+
exceptionLogger,
|
|
81
|
+
});
|
|
82
|
+
instrumentations.push(azureOpenAIInstrumentation);
|
|
83
|
+
|
|
84
|
+
cohereInstrumentation = new CohereInstrumentation({ exceptionLogger });
|
|
85
|
+
instrumentations.push(cohereInstrumentation);
|
|
86
|
+
|
|
87
|
+
vertexaiInstrumentation = new VertexAIInstrumentation({
|
|
88
|
+
exceptionLogger,
|
|
89
|
+
});
|
|
90
|
+
instrumentations.push(vertexaiInstrumentation);
|
|
91
|
+
|
|
92
|
+
aiplatformInstrumentation = new AIPlatformInstrumentation({
|
|
93
|
+
exceptionLogger,
|
|
94
|
+
});
|
|
95
|
+
instrumentations.push(aiplatformInstrumentation);
|
|
96
|
+
|
|
97
|
+
bedrockInstrumentation = new BedrockInstrumentation({ exceptionLogger });
|
|
98
|
+
instrumentations.push(bedrockInstrumentation);
|
|
99
|
+
|
|
100
|
+
pineconeInstrumentation = new PineconeInstrumentation({ exceptionLogger });
|
|
101
|
+
instrumentations.push(pineconeInstrumentation);
|
|
102
|
+
|
|
103
|
+
langchainInstrumentation = new LangChainInstrumentation({ exceptionLogger });
|
|
104
|
+
instrumentations.push(langchainInstrumentation);
|
|
105
|
+
|
|
106
|
+
llamaIndexInstrumentation = new LlamaIndexInstrumentation({
|
|
107
|
+
exceptionLogger,
|
|
108
|
+
});
|
|
109
|
+
instrumentations.push(llamaIndexInstrumentation);
|
|
110
|
+
|
|
111
|
+
chromadbInstrumentation = new ChromaDBInstrumentation({ exceptionLogger });
|
|
112
|
+
instrumentations.push(chromadbInstrumentation);
|
|
113
|
+
|
|
114
|
+
qdrantInstrumentation = new QdrantInstrumentation({ exceptionLogger });
|
|
115
|
+
instrumentations.push(qdrantInstrumentation);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const manuallyInitInstrumentations = (
|
|
119
|
+
instrumentModules: InitializeOptions["instrumentModules"],
|
|
120
|
+
) => {
|
|
121
|
+
const exceptionLogger = (e: Error) => Telemetry.getInstance().logException(e);
|
|
122
|
+
const enrichTokens =
|
|
123
|
+
(process.env.TRACELOOP_ENRICH_TOKENS || "true").toLowerCase() === "true";
|
|
124
|
+
|
|
125
|
+
instrumentations.length = 0;
|
|
126
|
+
|
|
127
|
+
if (instrumentModules?.openAI) {
|
|
128
|
+
openAIInstrumentation = new OpenAIInstrumentation({
|
|
129
|
+
enrichTokens,
|
|
130
|
+
exceptionLogger,
|
|
131
|
+
});
|
|
132
|
+
instrumentations.push(openAIInstrumentation);
|
|
133
|
+
openAIInstrumentation.manuallyInstrument(instrumentModules.openAI);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (instrumentModules?.anthropic) {
|
|
137
|
+
anthropicInstrumentation = new AnthropicInstrumentation({
|
|
138
|
+
exceptionLogger,
|
|
139
|
+
});
|
|
140
|
+
instrumentations.push(anthropicInstrumentation);
|
|
141
|
+
anthropicInstrumentation.manuallyInstrument(instrumentModules.anthropic);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (instrumentModules?.azureOpenAI) {
|
|
145
|
+
const instrumentation = new AzureOpenAIInstrumentation({ exceptionLogger });
|
|
146
|
+
instrumentations.push(instrumentation as Instrumentation);
|
|
147
|
+
azureOpenAIInstrumentation = instrumentation;
|
|
148
|
+
instrumentation.manuallyInstrument(instrumentModules.azureOpenAI);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (instrumentModules?.cohere) {
|
|
152
|
+
cohereInstrumentation = new CohereInstrumentation({ exceptionLogger });
|
|
153
|
+
instrumentations.push(cohereInstrumentation);
|
|
154
|
+
cohereInstrumentation.manuallyInstrument(instrumentModules.cohere);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (instrumentModules?.google_vertexai) {
|
|
158
|
+
vertexaiInstrumentation = new VertexAIInstrumentation({
|
|
159
|
+
exceptionLogger,
|
|
160
|
+
});
|
|
161
|
+
instrumentations.push(vertexaiInstrumentation);
|
|
162
|
+
vertexaiInstrumentation.manuallyInstrument(
|
|
163
|
+
instrumentModules.google_vertexai,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (instrumentModules?.google_aiplatform) {
|
|
168
|
+
aiplatformInstrumentation = new AIPlatformInstrumentation({
|
|
169
|
+
exceptionLogger,
|
|
170
|
+
});
|
|
171
|
+
instrumentations.push(aiplatformInstrumentation);
|
|
172
|
+
aiplatformInstrumentation.manuallyInstrument(
|
|
173
|
+
instrumentModules.google_aiplatform,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (instrumentModules?.bedrock) {
|
|
178
|
+
bedrockInstrumentation = new BedrockInstrumentation({ exceptionLogger });
|
|
179
|
+
instrumentations.push(bedrockInstrumentation);
|
|
180
|
+
bedrockInstrumentation.manuallyInstrument(instrumentModules.bedrock);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (instrumentModules?.pinecone) {
|
|
184
|
+
const instrumentation = new PineconeInstrumentation({ exceptionLogger });
|
|
185
|
+
instrumentations.push(instrumentation as Instrumentation);
|
|
186
|
+
instrumentation.manuallyInstrument(instrumentModules.pinecone);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (instrumentModules?.langchain) {
|
|
190
|
+
langchainInstrumentation = new LangChainInstrumentation({
|
|
191
|
+
exceptionLogger,
|
|
192
|
+
});
|
|
193
|
+
instrumentations.push(langchainInstrumentation);
|
|
194
|
+
langchainInstrumentation.manuallyInstrument(instrumentModules.langchain);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (instrumentModules?.llamaIndex) {
|
|
198
|
+
llamaIndexInstrumentation = new LlamaIndexInstrumentation({
|
|
199
|
+
exceptionLogger,
|
|
200
|
+
});
|
|
201
|
+
instrumentations.push(llamaIndexInstrumentation);
|
|
202
|
+
llamaIndexInstrumentation.manuallyInstrument(instrumentModules.llamaIndex);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (instrumentModules?.chromadb) {
|
|
206
|
+
chromadbInstrumentation = new ChromaDBInstrumentation({ exceptionLogger });
|
|
207
|
+
instrumentations.push(chromadbInstrumentation);
|
|
208
|
+
chromadbInstrumentation.manuallyInstrument(instrumentModules.chromadb);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (instrumentModules?.qdrant) {
|
|
212
|
+
qdrantInstrumentation = new QdrantInstrumentation({ exceptionLogger });
|
|
213
|
+
instrumentations.push(qdrantInstrumentation);
|
|
214
|
+
qdrantInstrumentation.manuallyInstrument(instrumentModules.qdrant);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
function awaitAttributes(detector: DetectorSync): Detector {
|
|
219
|
+
return {
|
|
220
|
+
async detect(config?: ResourceDetectionConfig): Promise<IResource> {
|
|
221
|
+
const resource = detector.detect(config)
|
|
222
|
+
await resource.waitForAsyncAttributes?.()
|
|
223
|
+
|
|
224
|
+
return resource
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Initializes the Traceloop SDK.
|
|
231
|
+
* Must be called once before any other SDK methods.
|
|
232
|
+
*
|
|
233
|
+
* @param options - The options to initialize the SDK. See the {@link InitializeOptions} for details.
|
|
234
|
+
*/
|
|
235
|
+
export const startTracing = (options: InitializeOptions) => {
|
|
236
|
+
if (options.instrumentModules !== undefined) {
|
|
237
|
+
// If options.instrumentModules is empty, it will not initialize anything,
|
|
238
|
+
// so empty dict can essentially be passed to disable any kind of automatic instrumentation.
|
|
239
|
+
manuallyInitInstrumentations(options.instrumentModules);
|
|
240
|
+
} else {
|
|
241
|
+
initInstrumentations();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!shouldSendTraces()) {
|
|
245
|
+
openAIInstrumentation?.setConfig({
|
|
246
|
+
traceContent: false,
|
|
247
|
+
});
|
|
248
|
+
azureOpenAIInstrumentation?.setConfig({
|
|
249
|
+
traceContent: false,
|
|
250
|
+
});
|
|
251
|
+
llamaIndexInstrumentation?.setConfig({
|
|
252
|
+
traceContent: false,
|
|
253
|
+
});
|
|
254
|
+
vertexaiInstrumentation?.setConfig({
|
|
255
|
+
traceContent: false,
|
|
256
|
+
});
|
|
257
|
+
aiplatformInstrumentation?.setConfig({
|
|
258
|
+
traceContent: false,
|
|
259
|
+
});
|
|
260
|
+
bedrockInstrumentation?.setConfig({
|
|
261
|
+
traceContent: false,
|
|
262
|
+
});
|
|
263
|
+
cohereInstrumentation?.setConfig({
|
|
264
|
+
traceContent: false,
|
|
265
|
+
});
|
|
266
|
+
chromadbInstrumentation?.setConfig({
|
|
267
|
+
traceContent: false,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const headers = process.env.TRACELOOP_HEADERS
|
|
272
|
+
? baggageUtils.parseKeyPairsIntoRecord(process.env.TRACELOOP_HEADERS)
|
|
273
|
+
: { Authorization: `Bearer ${options.apiKey}` };
|
|
274
|
+
|
|
275
|
+
const traceExporter =
|
|
276
|
+
options.exporter ??
|
|
277
|
+
new OTLPTraceExporter({
|
|
278
|
+
url: `${options.baseUrl}/v1/traces`,
|
|
279
|
+
headers,
|
|
280
|
+
});
|
|
281
|
+
_spanProcessor = options.disableBatch
|
|
282
|
+
? new SimpleSpanProcessor(traceExporter)
|
|
283
|
+
: new BatchSpanProcessor(traceExporter);
|
|
284
|
+
|
|
285
|
+
_spanProcessor.onStart = (span: Span) => {
|
|
286
|
+
// This sets the properties only if the context has them
|
|
287
|
+
const associationProperties = context
|
|
288
|
+
.active()
|
|
289
|
+
.getValue(ASSOCIATION_PROPERTIES_KEY);
|
|
290
|
+
if (associationProperties) {
|
|
291
|
+
for (const [key, value] of Object.entries(associationProperties)) {
|
|
292
|
+
span.setAttribute(
|
|
293
|
+
`${SpanAttributes.TRACELOOP_ASSOCIATION_PROPERTIES}.${key}`,
|
|
294
|
+
value,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
if (options.exporter) {
|
|
301
|
+
Telemetry.getInstance().capture("tracer:init", {
|
|
302
|
+
exporter: "custom",
|
|
303
|
+
processor: options.disableBatch ? "simple" : "batch",
|
|
304
|
+
});
|
|
305
|
+
} else {
|
|
306
|
+
Telemetry.getInstance().capture("tracer:init", {
|
|
307
|
+
exporter: options.baseUrl ?? "",
|
|
308
|
+
processor: options.disableBatch ? "simple" : "batch",
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const spanProcessors: SpanProcessor[] = [_spanProcessor];
|
|
313
|
+
if (options.processor) {
|
|
314
|
+
spanProcessors.push(options.processor);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
_sdk = new NodeSDK({
|
|
318
|
+
resource: new Resource({
|
|
319
|
+
[SEMRESATTRS_SERVICE_NAME]:
|
|
320
|
+
options.appName || process.env.npm_package_name,
|
|
321
|
+
}),
|
|
322
|
+
spanProcessors,
|
|
323
|
+
contextManager: options.contextManager,
|
|
324
|
+
textMapPropagator: options.propagator,
|
|
325
|
+
traceExporter,
|
|
326
|
+
instrumentations,
|
|
327
|
+
// We should re-consider removing irrelevant spans here in the future
|
|
328
|
+
// sampler: new TraceloopSampler(),
|
|
329
|
+
resourceDetectors: [
|
|
330
|
+
awaitAttributes(envDetectorSync),
|
|
331
|
+
awaitAttributes(processDetectorSync),
|
|
332
|
+
awaitAttributes(hostDetectorSync),
|
|
333
|
+
],
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
_sdk.start();
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
export const shouldSendTraces = () => {
|
|
340
|
+
if (!_configuration) {
|
|
341
|
+
diag.warn("Traceloop not initialized");
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (
|
|
346
|
+
_configuration.traceContent === false ||
|
|
347
|
+
(process.env.TRACELOOP_TRACE_CONTENT || "true").toLowerCase() === "false"
|
|
348
|
+
) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return true;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export const forceFlush = async () => {
|
|
356
|
+
await _spanProcessor.forceFlush();
|
|
357
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Span, context, trace } from "@opentelemetry/api";
|
|
2
|
+
import { getTracer } from "./tracing";
|
|
3
|
+
import {
|
|
4
|
+
Events,
|
|
5
|
+
EventAttributes,
|
|
6
|
+
SpanAttributes,
|
|
7
|
+
} from "@traceloop/ai-semantic-conventions";
|
|
8
|
+
import { shouldSendTraces } from ".";
|
|
9
|
+
|
|
10
|
+
type VectorDBCallConfig = {
|
|
11
|
+
vendor: string;
|
|
12
|
+
type: "query" | "upsert" | "delete";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type LLMCallConfig = {
|
|
16
|
+
vendor: string;
|
|
17
|
+
type: "chat" | "completion";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class VectorSpan {
|
|
21
|
+
private span: Span;
|
|
22
|
+
|
|
23
|
+
constructor(span: Span) {
|
|
24
|
+
this.span = span;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
reportQuery({ queryVector }: { queryVector: number[] }) {
|
|
28
|
+
if (!shouldSendTraces()) {
|
|
29
|
+
this.span.addEvent(Events.DB_QUERY_EMBEDDINGS);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.span.addEvent(Events.DB_QUERY_EMBEDDINGS, {
|
|
33
|
+
[EventAttributes.DB_QUERY_EMBEDDINGS_VECTOR]: JSON.stringify(queryVector),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
reportResults({
|
|
38
|
+
results,
|
|
39
|
+
}: {
|
|
40
|
+
results: {
|
|
41
|
+
ids?: string;
|
|
42
|
+
scores?: number;
|
|
43
|
+
distances?: number;
|
|
44
|
+
metadata?: Record<string, unknown>;
|
|
45
|
+
vectors?: number[];
|
|
46
|
+
documents?: string;
|
|
47
|
+
}[];
|
|
48
|
+
}) {
|
|
49
|
+
for (let i = 0; i < results.length; i++) {
|
|
50
|
+
this.span.addEvent(Events.DB_QUERY_RESULT, {
|
|
51
|
+
[EventAttributes.DB_QUERY_RESULT_ID]: results[i].ids,
|
|
52
|
+
[EventAttributes.DB_QUERY_RESULT_SCORE]: results[i].scores,
|
|
53
|
+
[EventAttributes.DB_QUERY_RESULT_DISTANCE]: results[i].distances,
|
|
54
|
+
[EventAttributes.DB_QUERY_RESULT_METADATA]: JSON.stringify(
|
|
55
|
+
results[i].metadata,
|
|
56
|
+
),
|
|
57
|
+
[EventAttributes.DB_QUERY_RESULT_VECTOR]: results[i].vectors,
|
|
58
|
+
[EventAttributes.DB_QUERY_RESULT_DOCUMENT]: results[i].documents,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class LLMSpan {
|
|
65
|
+
private span: Span;
|
|
66
|
+
|
|
67
|
+
constructor(span: Span) {
|
|
68
|
+
this.span = span;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
reportRequest({
|
|
72
|
+
model,
|
|
73
|
+
messages,
|
|
74
|
+
}: {
|
|
75
|
+
model: string;
|
|
76
|
+
messages: {
|
|
77
|
+
role: string;
|
|
78
|
+
content?: string | unknown;
|
|
79
|
+
}[];
|
|
80
|
+
}) {
|
|
81
|
+
this.span.setAttributes({
|
|
82
|
+
[SpanAttributes.LLM_REQUEST_MODEL]: model,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
messages.forEach((message, index) => {
|
|
86
|
+
this.span.setAttributes({
|
|
87
|
+
[`${SpanAttributes.LLM_PROMPTS}.${index}.role`]: message.role,
|
|
88
|
+
[`${SpanAttributes.LLM_PROMPTS}.${index}.content`]:
|
|
89
|
+
typeof message.content === "string"
|
|
90
|
+
? message.content
|
|
91
|
+
: JSON.stringify(message.content),
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
reportResponse({
|
|
97
|
+
model,
|
|
98
|
+
usage,
|
|
99
|
+
completions,
|
|
100
|
+
}: {
|
|
101
|
+
model: string;
|
|
102
|
+
usage?: {
|
|
103
|
+
prompt_tokens: number;
|
|
104
|
+
completion_tokens: number;
|
|
105
|
+
total_tokens: number;
|
|
106
|
+
};
|
|
107
|
+
completions?: {
|
|
108
|
+
finish_reason: string;
|
|
109
|
+
message: {
|
|
110
|
+
role: "system" | "user" | "assistant";
|
|
111
|
+
content: string | null;
|
|
112
|
+
};
|
|
113
|
+
}[];
|
|
114
|
+
}) {
|
|
115
|
+
this.span.setAttribute(SpanAttributes.LLM_RESPONSE_MODEL, model);
|
|
116
|
+
|
|
117
|
+
if (usage) {
|
|
118
|
+
this.span.setAttributes({
|
|
119
|
+
[SpanAttributes.LLM_USAGE_PROMPT_TOKENS]: usage.prompt_tokens,
|
|
120
|
+
[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]: usage.completion_tokens,
|
|
121
|
+
[SpanAttributes.LLM_USAGE_TOTAL_TOKENS]: usage.total_tokens,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
completions?.forEach((completion, index) => {
|
|
126
|
+
this.span.setAttributes({
|
|
127
|
+
[`${SpanAttributes.LLM_COMPLETIONS}.${index}.finish_reason`]:
|
|
128
|
+
completion.finish_reason,
|
|
129
|
+
[`${SpanAttributes.LLM_COMPLETIONS}.${index}.role`]:
|
|
130
|
+
completion.message.role,
|
|
131
|
+
[`${SpanAttributes.LLM_COMPLETIONS}.${index}.content`]:
|
|
132
|
+
completion.message.content || "",
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function withVectorDBCall<
|
|
139
|
+
F extends ({ span }: { span: VectorSpan }) => ReturnType<F>,
|
|
140
|
+
>({ vendor, type }: VectorDBCallConfig, fn: F, thisArg?: ThisParameterType<F>) {
|
|
141
|
+
const entityContext = context.active();
|
|
142
|
+
|
|
143
|
+
return getTracer().startActiveSpan(
|
|
144
|
+
`${vendor}.${type}`,
|
|
145
|
+
{ [SpanAttributes.LLM_REQUEST_TYPE]: type },
|
|
146
|
+
entityContext,
|
|
147
|
+
(span: Span) => {
|
|
148
|
+
const res = fn.apply(thisArg, [{ span: new VectorSpan(span) }]);
|
|
149
|
+
if (res instanceof Promise) {
|
|
150
|
+
return res.then((resolvedRes) => {
|
|
151
|
+
span.end();
|
|
152
|
+
return resolvedRes;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
span.end();
|
|
157
|
+
return res;
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function withLLMCall<
|
|
163
|
+
F extends ({ span }: { span: LLMSpan }) => ReturnType<F>,
|
|
164
|
+
>({ vendor, type }: LLMCallConfig, fn: F, thisArg?: ThisParameterType<F>) {
|
|
165
|
+
const span = getTracer().startSpan(`${vendor}.${type}`, {}, context.active());
|
|
166
|
+
span.setAttribute(SpanAttributes.LLM_REQUEST_TYPE, type);
|
|
167
|
+
trace.setSpan(context.active(), span);
|
|
168
|
+
|
|
169
|
+
const res = fn.apply(thisArg, [{ span: new LLMSpan(span) }]);
|
|
170
|
+
if (res instanceof Promise) {
|
|
171
|
+
return res.then((resolvedRes) => {
|
|
172
|
+
span.end();
|
|
173
|
+
return resolvedRes;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
span.end();
|
|
178
|
+
return res;
|
|
179
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Sampler,
|
|
3
|
+
SamplingResult,
|
|
4
|
+
SamplingDecision,
|
|
5
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
6
|
+
import { Context, SpanKind, Attributes } from "@opentelemetry/api";
|
|
7
|
+
|
|
8
|
+
const FILTERED_ATTRIBUTE_KEYS = ["next.span_name"];
|
|
9
|
+
|
|
10
|
+
export class TraceloopSampler implements Sampler {
|
|
11
|
+
shouldSample(
|
|
12
|
+
_context: Context,
|
|
13
|
+
_traceId: string,
|
|
14
|
+
_spanName: string,
|
|
15
|
+
_spanKind: SpanKind,
|
|
16
|
+
attributes: Attributes,
|
|
17
|
+
): SamplingResult {
|
|
18
|
+
let filter = false;
|
|
19
|
+
FILTERED_ATTRIBUTE_KEYS.forEach((key) => {
|
|
20
|
+
if (attributes?.[key]) {
|
|
21
|
+
filter = true;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
decision: filter
|
|
27
|
+
? SamplingDecision.NOT_RECORD
|
|
28
|
+
: SamplingDecision.RECORD_AND_SAMPLED,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
toString(): string {
|
|
33
|
+
return "TraceloopSampler";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { trace, createContextKey } from "@opentelemetry/api";
|
|
2
|
+
import { Context } from "@opentelemetry/api/build/src/context/types";
|
|
3
|
+
|
|
4
|
+
const TRACER_NAME = "traceloop.tracer";
|
|
5
|
+
export const WORKFLOW_NAME_KEY = createContextKey("workflow_name");
|
|
6
|
+
export const ENTITY_NAME_KEY = createContextKey("entity_name");
|
|
7
|
+
export const ASSOCIATION_PROPERTIES_KEY = createContextKey(
|
|
8
|
+
"association_properties",
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export const getTracer = () => {
|
|
12
|
+
return trace.getTracer(TRACER_NAME);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getEntityPath = (entityContext: Context): string | undefined => {
|
|
16
|
+
const path = entityContext.getValue(ENTITY_NAME_KEY);
|
|
17
|
+
|
|
18
|
+
return path ? `${path}` : undefined;
|
|
19
|
+
};
|