@lmnr-ai/lmnr 0.4.5 → 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 +3 -3
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +14 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +39 -3
- package/src/decorators.ts +3 -6
- package/src/laminar.ts +48 -8
- 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/src/laminar.ts
CHANGED
|
@@ -1,8 +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
|
+
import { Metadata } from '@grpc/grpc-js';
|
|
7
|
+
import { ASSOCIATION_PROPERTIES_KEY } from './sdk/tracing/tracing';
|
|
6
8
|
|
|
7
9
|
// quick patch to get the traceloop's default tracer, since their
|
|
8
10
|
// `getTracer` function is not exported.
|
|
@@ -19,7 +21,7 @@ interface LaminarInitializeProps {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export class Laminar {
|
|
22
|
-
private static baseUrl: string = 'https://api.lmnr.ai';
|
|
24
|
+
private static baseUrl: string = 'https://api.lmnr.ai:8443';
|
|
23
25
|
private static projectApiKey: string;
|
|
24
26
|
private static env: Record<string, string> = {};
|
|
25
27
|
private static isInitialized: boolean = false;
|
|
@@ -34,8 +36,28 @@ export class Laminar {
|
|
|
34
36
|
* @param env - Default environment passed to `run` and `evaluateEvent` requests,
|
|
35
37
|
* unless overriden at request time. Usually, model provider keys are stored here.
|
|
36
38
|
* @param baseUrl - Url of Laminar endpoint, or the custom open telemetry ingester.
|
|
37
|
-
* If not specified, defaults to https://api.lmnr.ai. For locally hosted Laminar,
|
|
38
|
-
* default setting must be http://localhost:
|
|
39
|
+
* If not specified, defaults to https://api.lmnr.ai:8443. For locally hosted Laminar,
|
|
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
|
+
* ```
|
|
39
61
|
*
|
|
40
62
|
* @throws {Error} - If project API key is not set
|
|
41
63
|
*/
|
|
@@ -59,11 +81,19 @@ export class Laminar {
|
|
|
59
81
|
}
|
|
60
82
|
this.isInitialized = true;
|
|
61
83
|
this.env = env ?? {};
|
|
84
|
+
|
|
85
|
+
const metadata = new Metadata();
|
|
86
|
+
metadata.set('authorization', `Bearer ${this.projectApiKey}`);
|
|
87
|
+
const exporter = new OTLPTraceExporter({
|
|
88
|
+
url: this.baseUrl,
|
|
89
|
+
metadata,
|
|
90
|
+
});
|
|
91
|
+
|
|
62
92
|
traceloopInitialize({
|
|
63
|
-
|
|
64
|
-
baseUrl: this.baseUrl,
|
|
93
|
+
exporter,
|
|
65
94
|
silenceInitializationMessage: true,
|
|
66
95
|
instrumentModules,
|
|
96
|
+
disableBatch: false,
|
|
67
97
|
});
|
|
68
98
|
}
|
|
69
99
|
|
|
@@ -267,7 +297,17 @@ export class Laminar {
|
|
|
267
297
|
if (userId) {
|
|
268
298
|
associationProperties = { ...associationProperties, "user_id": userId };
|
|
269
299
|
}
|
|
270
|
-
|
|
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;
|
|
271
311
|
}
|
|
272
312
|
|
|
273
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
|
+
}
|