diff --git a/package-lock.json b/package-lock.json index 2aa92e1..97a9866 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,20 @@ { "name": "netra-sdk", - "version": "1.0.4", + "version": "1.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "netra-sdk", - "version": "1.0.4", + "version": "1.0.5", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.0.1", + "@opentelemetry/exporter-metrics-otlp-http": "^0.208.0", "@opentelemetry/otlp-transformer": "^0.211.0", "@opentelemetry/resources": "^2.0.1", + "@opentelemetry/sdk-metrics": "^2.0.1", "@opentelemetry/sdk-trace-base": "^2.0.1", "@traceloop/node-server-sdk": "^0.22.5", "axios": "^1.13.4", @@ -1170,6 +1172,25 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.203.0.tgz", + "integrity": "sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-metrics": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { "version": "0.203.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.203.0.tgz", @@ -1225,16 +1246,16 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.203.0.tgz", - "integrity": "sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ==", + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.208.0.tgz", + "integrity": "sha512-QZ3TrI90Y0i1ezWQdvreryjY0a5TK4J9gyDLIyhLBwV+EQUvyp5wR7TFPKCAexD4TDSWM0t3ulQDbYYjVtzTyA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.1", - "@opentelemetry/otlp-exporter-base": "0.203.0", - "@opentelemetry/otlp-transformer": "0.203.0", - "@opentelemetry/resources": "2.0.1", - "@opentelemetry/sdk-metrics": "2.0.1" + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-exporter-base": "0.208.0", + "@opentelemetry/otlp-transformer": "0.208.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-metrics": "2.2.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1243,10 +1264,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/api-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", + "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", - "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -1258,18 +1291,34 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz", + "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-transformer": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/otlp-transformer": { - "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.203.0.tgz", - "integrity": "sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A==", + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz", + "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.203.0", - "@opentelemetry/core": "2.0.1", - "@opentelemetry/resources": "2.0.1", - "@opentelemetry/sdk-logs": "0.203.0", - "@opentelemetry/sdk-metrics": "2.0.1", - "@opentelemetry/sdk-trace-base": "2.0.1", + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-logs": "0.208.0", + "@opentelemetry/sdk-metrics": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "engines": { @@ -1280,12 +1329,12 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", - "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.1", + "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1295,14 +1344,47 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz", + "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz", - "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.0.1", - "@opentelemetry/resources": "2.0.1", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1347,6 +1429,25 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.203.0.tgz", + "integrity": "sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-metrics": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/otlp-transformer": { "version": "0.203.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.203.0.tgz", @@ -2342,6 +2443,46 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.203.0.tgz", + "integrity": "sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-metrics": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.203.0.tgz", + "integrity": "sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-logs": "0.203.0", + "@opentelemetry/sdk-metrics": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/resources": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", diff --git a/package.json b/package.json index 57681ad..d2e0663 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,10 @@ "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.0.1", + "@opentelemetry/exporter-metrics-otlp-http": "^0.208.0", "@opentelemetry/otlp-transformer": "^0.211.0", "@opentelemetry/resources": "^2.0.1", + "@opentelemetry/sdk-metrics": "^2.0.1", "@opentelemetry/sdk-trace-base": "^2.0.1", "@traceloop/node-server-sdk": "^0.22.5", "axios": "^1.13.4", diff --git a/src/api/dashboard/api.ts b/src/api/dashboard/api.ts index 6c8bb18..10e67f5 100644 --- a/src/api/dashboard/api.ts +++ b/src/api/dashboard/api.ts @@ -9,6 +9,7 @@ import { ChartType, Dimension, FilterConfig, + Measure, Metrics, QueryDataParams, QueryResponse, @@ -204,12 +205,24 @@ async getSessionStats( } private isValidMetrics(metrics: any): metrics is Metrics { - return ( + const hasBaseShape = metrics && typeof metrics === "object" && "measure" in metrics && - "aggregation" in metrics - ); + "aggregation" in metrics; + + if (!hasBaseShape) { + return false; + } + + if (metrics.measure === Measure.CUSTOM) { + return ( + typeof metrics.metricName === "string" && + metrics.metricName.trim().length > 0 + ); + } + + return true; } private isValidFilterConfig(filter: any): filter is FilterConfig { diff --git a/src/api/dashboard/client.ts b/src/api/dashboard/client.ts index cdc30de..2a4c84d 100644 --- a/src/api/dashboard/client.ts +++ b/src/api/dashboard/client.ts @@ -43,6 +43,7 @@ export class DashboardHttpClient extends NetraHttpClient { metrics: { measure: metrics.measure, aggregation: metrics.aggregation, + ...(metrics.metricName ? { metricName: metrics.metricName } : {}), }, }; diff --git a/src/api/dashboard/models.ts b/src/api/dashboard/models.ts index 5a8aa0d..23cd227 100644 --- a/src/api/dashboard/models.ts +++ b/src/api/dashboard/models.ts @@ -26,6 +26,7 @@ export enum Measure { TOTAL_TOKENS = "Total Tokens", AUDIO_DURATION = "Audio Duration", CHARACTER_COUNT = "Character Count", + CUSTOM = "Custom", } export enum Aggregation { @@ -133,6 +134,7 @@ export interface Filter { export interface Metrics { measure: Measure; aggregation: Aggregation; + metricName?: string; } export interface Dimension { diff --git a/src/api/index.ts b/src/api/index.ts index ab29e55..6dc1451 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -5,62 +5,66 @@ // Usage API export { Usage } from "./usage"; export type { - ListSpansParams, - ListTracesParams, - SessionUsageData, - SpansPage, - TenantUsageData, - TraceSpan, - TracesPage, - TraceSummary, + ListSpansParams, + ListTracesParams, + SessionUsageData, + SpansPage, + TenantUsageData, + TraceSpan, + TracesPage, + TraceSummary, } from "./usage"; // Evaluation API export { - Evaluation, - EntryStatus, - RunStatus, - RunEntryContext, + Evaluation, + EntryStatus, + RunStatus, + RunEntryContext, } from "./evaluation"; export type { - CreateDatasetParams, - Dataset, - DatasetEntry, - DatasetItem, - EvaluationScore, - EvaluatorFunction, - Run, - TaskFunction, - TestSuiteResult, + CreateDatasetParams, + Dataset, + DatasetEntry, + DatasetItem, + EvaluationScore, + EvaluatorFunction, + Run, + TaskFunction, + TestSuiteResult, } from "./evaluation"; // Dashboard API export { - Dashboard, - Aggregation, - ChartType, - DimensionField, - FilterField, - FilterType, - GroupBy, - Measure, - metadataField, - Operator, - Scope, + Dashboard, + Aggregation, + ChartType, + DimensionField, + FilterField, + FilterType, + GroupBy, + Measure, + metadataField, + Operator, + Scope, } from "./dashboard"; export type { - CategoricalDataPoint, - DashboardData, - Dimension, - DimensionValue, - Filter, - FilterConfig, - Metrics, - NumberResponse, - QueryDataParams, - QueryResponse, - TimeRange, - TimeSeriesDataPoint, - TimeSeriesResponse, - TimeSeriesWithDimension, + CategoricalDataPoint, + DashboardData, + Dimension, + DimensionValue, + Filter, + FilterConfig, + Metrics, + NumberResponse, + QueryDataParams, + QueryResponse, + TimeRange, + TimeSeriesDataPoint, + TimeSeriesResponse, + TimeSeriesWithDimension, } from "./dashboard"; + +// Prompts API +export { Prompts } from "./prompts"; +export type { GetPromptParams, PromptResponse } from "./prompts"; diff --git a/src/api/prompts/api.ts b/src/api/prompts/api.ts new file mode 100644 index 0000000..c8438ff --- /dev/null +++ b/src/api/prompts/api.ts @@ -0,0 +1,44 @@ +import { Config } from "../../config"; +import { PromptsHttpClient } from "./client"; +import { GetPromptParams, PromptResponse } from "./models"; + +export class Prompts { + private config: Config; + private client: PromptsHttpClient; + + constructor(config: Config) { + this.config = config; + this.client = new PromptsHttpClient(config); + } + + /** + * Fetch prompt version by name + label + */ + async getPrompt(params: GetPromptParams): Promise { + if (!this.isValidParams(params)) { + throw new TypeError( + "params must contain { name: string, label: string }", + ); + } + + const result = await this.client.getPromptVersion( + params.name, + params.label, + ); + + if (!result) { + return null; + } + + return result.data ?? null; + } + + private isValidParams(value: any): value is GetPromptParams { + return ( + value && + typeof value === "object" && + typeof value.name === "string" && + typeof value.label === "string" + ); + } +} diff --git a/src/api/prompts/client.ts b/src/api/prompts/client.ts new file mode 100644 index 0000000..0eea494 --- /dev/null +++ b/src/api/prompts/client.ts @@ -0,0 +1,49 @@ +/** + * Internal HTTP client for Prompts APIs + */ + +import { Config } from "../../config"; +import { NetraHttpClient } from "../http-client"; + +export class PromptsHttpClient extends NetraHttpClient { + constructor(config: Config) { + super(config, "NETRA_PROMPTS_TIMEOUT", 30.0); + } + + /** + * Fetch prompt version from backend + */ + async getPromptVersion(name: string, label: string): Promise { + if (!this.isInitialized()) { + console.error( + "netra.prompts: Prompts client is not initialized; cannot fetch prompt", + ); + return null; + } + + try { + const payload = { + promptName: name, + label: label, + }; + + const response = await this.post("/sdk/prompts/version", payload); + + if (!response.ok) { + const errorMessage = response.data?.error?.message ?? "Unknown error"; + + console.error(`netra.prompts: Failed to fetch prompt: ${errorMessage}`); + + return null; + } + + return response.data; + } catch (err: any) { + const message = err?.response?.data?.error?.message ?? ""; + + console.error("netra.prompts: Failed to fetch prompt:", message); + + return null; + } + } +} diff --git a/src/api/prompts/index.ts b/src/api/prompts/index.ts new file mode 100644 index 0000000..bedb08f --- /dev/null +++ b/src/api/prompts/index.ts @@ -0,0 +1,7 @@ +/** + * Prompts API exports + */ + +export { Prompts } from "./api"; + +export type { GetPromptParams, PromptResponse } from "./models"; diff --git a/src/api/prompts/models.ts b/src/api/prompts/models.ts new file mode 100644 index 0000000..42b1f7d --- /dev/null +++ b/src/api/prompts/models.ts @@ -0,0 +1,14 @@ +/** + * Prompts API Models + */ + +export interface GetPromptParams { + name: string; + label: string; +} + +/** + * Prompt response is intentionally flexible because + * backend prompt structures may evolve (variables, templates, metadata etc.) + */ +export type PromptResponse = Record; diff --git a/src/config.ts b/src/config.ts index 8361b2b..4da3113 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,6 +12,8 @@ export interface NetraConfig { resourceAttributes?: Record; environment?: string; enableScrubbing?: boolean; + enableMetrics?: boolean; + metricsExportIntervalMs?: number; blockedSpans?: string[]; instruments?: Set; blockInstruments?: Set; @@ -80,6 +82,8 @@ export class Config { debugMode: boolean; enableRootSpan: boolean; enableScrubbing: boolean; + enableMetrics: boolean; + metricsExportIntervalMs: number; environment: string; resourceAttributes: Record; blockedSpans?: string[]; @@ -114,6 +118,16 @@ export class Config { "NETRA_ENABLE_SCRUBBING", false, ); + this.enableMetrics = this._getBoolConfig( + config.enableMetrics, + "NETRA_ENABLE_METRICS", + false, + ); + this.metricsExportIntervalMs = this._getNumberConfig( + config.metricsExportIntervalMs, + "NETRA_METRICS_EXPORT_INTERVAL", + 60000, + ); this.environment = config.environment || process.env.NETRA_ENV || "local"; this.resourceAttributes = this._getResourceAttributes( config.resourceAttributes, @@ -235,6 +249,24 @@ export class Config { } } + private _getNumberConfig( + param: number | undefined, + envVar: string, + defaultValue: number, + ): number { + if (param !== undefined) { + return param; + } + + const envValue = process.env[envVar]; + if (envValue === undefined) { + return defaultValue; + } + + const parsed = Number.parseInt(envValue, 10); + return Number.isFinite(parsed) ? parsed : defaultValue; + } + private _setTraceContentEnv(): void { process.env.TRACELOOP_TRACE_CONTENT = this.traceContent ? "true" : "false"; } @@ -260,6 +292,24 @@ export class Config { return cleanUrl; } + /** + * Format the OTLP endpoint URL by appending /v1/metrics if not already present + */ + public formatOtlpMetricsEndpoint(): string | undefined { + if (!this.otlpEndpoint) { + return undefined; + } + + const url = this.otlpEndpoint.trim(); + const cleanUrl = url.endsWith("/") ? url.slice(0, -1) : url; + + if (!cleanUrl.endsWith("/v1/metrics")) { + return `${cleanUrl}/v1/metrics`; + } + + return cleanUrl; + } + /** * Set Traceloop environment variables based on Netra config. * This ensures the Traceloop SDK picks up our configuration. diff --git a/src/index.ts b/src/index.ts index 08b577f..75e7ce4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,10 +6,13 @@ import { context, + Meter, + metrics, Span, SpanKind, trace } from "@opentelemetry/api"; +import { MeterProvider } from "@opentelemetry/sdk-metrics"; import { createRequire } from "module"; import { Dashboard } from "./api/dashboard"; import { Evaluation } from "./api/evaluation"; @@ -29,14 +32,16 @@ import { } from "./session-manager"; import { Simulation } from "./simulation"; import { SpanWrapper } from "./span-wrapper"; +import { MeterSetup } from "./meter"; import { SpanType } from "./types"; +import { Prompts } from "./api"; export { Config, NetraInstruments } from "./config"; export { agent, span, task, workflow } from "./decorators"; export { InstrumentationSpanProcessor, ScrubbingSpanProcessor, - SessionSpanProcessor + SessionSpanProcessor, } from "./processors"; export { ConversationType } from "./session-manager"; export { SpanType } from "./types"; @@ -64,7 +69,9 @@ export { RunStatus, Scope, // Usage API - Usage + Usage, + // Prompts API + Prompts, } from "./api"; export type { @@ -101,7 +108,9 @@ export type { TimeSeriesWithDimension, TracesPage, TraceSpan, - TraceSummary + TraceSummary, + GetPromptParams, + PromptResponse, } from "./api"; // Export simulation types and classes @@ -126,6 +135,7 @@ export class Netra { private static _config: Config | undefined; private static _tracer: any; + private static _meterProvider: MeterProvider | undefined; /** * Usage API client for usage and traces @@ -147,6 +157,12 @@ export class Netra { static simulation: Simulation; + /** + * Prompts API client for prompt versioning + * Available after calling Netra.init() + */ + static prompts: Prompts; + /** * Get the current Netra configuration */ @@ -164,6 +180,24 @@ export class Netra { return this._initialized; } + /** + * Get an OpenTelemetry meter for creating custom metrics instruments. + */ + static getMeter(name: string, version: string = "1.0.0"): Meter { + if (!Netra._initialized || !Netra._config) { + throw new Error("Netra SDK not initialized. Call Netra.init() first."); + } + + if (!Netra._config.enableMetrics || !Netra._meterProvider) { + throw new Error( + "Metrics are not enabled. Set enableMetrics: true in Netra.init() config.", + ); + } + + // Standard OTel usage: obtain meter from global Metrics API. + return metrics.getMeter(name, version); + } + /** * Initialize the Netra SDK * @@ -212,11 +246,21 @@ export class Netra { ); } + if (cfg.enableMetrics) { + this._meterProvider = MeterSetup.setup(cfg); + if (cfg.debugMode) { + console.debug( + `[Netra Debug] metrics enabled=${Boolean(this._meterProvider)} endpoint=${cfg.formatOtlpMetricsEndpoint()}`, + ); + } + } + // Initialize API clients this.usage = new Usage(cfg); this.evaluation = new Evaluation(cfg); this.dashboard = new Dashboard(cfg); this.simulation = new Simulation(cfg); + this.prompts = new Prompts(cfg); this._initialized = true; console.info("Netra successfully initialized."); @@ -353,11 +397,33 @@ export class Netra { } try { - const provider = trace.getTracerProvider(); - - // Define timeouts const FLUSH_TIMEOUT_MS = 5000; + if (this._meterProvider) { + const meterProvider = this._meterProvider; + const meterFlushPromise = (async () => { + if ( + "forceFlush" in meterProvider && + typeof meterProvider.forceFlush === "function" + ) { + await meterProvider.forceFlush(); + } + if ( + "shutdown" in meterProvider && + typeof meterProvider.shutdown === "function" + ) { + await meterProvider.shutdown(); + } + })(); + + await Promise.race([ + meterFlushPromise, + new Promise((resolve) => setTimeout(resolve, FLUSH_TIMEOUT_MS)), + ]); + } + + const provider = trace.getTracerProvider(); + const flushPromise = (async () => { if ( "forceFlush" in provider && @@ -383,6 +449,7 @@ export class Netra { this._initialized = false; this._tracer = undefined; + this._meterProvider = undefined; } /** diff --git a/src/meter.ts b/src/meter.ts new file mode 100644 index 0000000..d58aca5 --- /dev/null +++ b/src/meter.ts @@ -0,0 +1,44 @@ +import { metrics } from "@opentelemetry/api"; +import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"; +import { + MeterProvider, + PeriodicExportingMetricReader +} from "@opentelemetry/sdk-metrics"; +import { Config } from "./config"; + +export class MeterSetup { + static setup(config: Config): MeterProvider | undefined { + if (!config.enableMetrics) { + return undefined; + } + + const endpoint = config.formatOtlpMetricsEndpoint(); + if (!endpoint) { + console.warn( + "Metrics enabled but OTLP endpoint is not configured; skipping meter setup.", + ); + return undefined; + } + + const exporterOptions: Record = { + url: endpoint, + headers: config.headers + }; + + // Keep this as a runtime-compatible assignment for OTel package version differences. + (exporterOptions as any).temporalityPreference = "delta"; + + const exporter = new OTLPMetricExporter(exporterOptions as any); + const reader = new PeriodicExportingMetricReader({ + exporter, + exportIntervalMillis: config.metricsExportIntervalMs + }); + + const meterProvider = new MeterProvider({ + readers: [reader] + }); + + metrics.setGlobalMeterProvider(meterProvider); + return meterProvider; + } +}