Skip to content

Commit 687005c

Browse files
committed
feat: Add OpenTelemetry Analytics Provider
1 parent 14b6e41 commit 687005c

26 files changed

Lines changed: 3436 additions & 53 deletions
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { INodeParams, INodeCredential } from '../src/Interface'
2+
3+
const VENDOR_PRESETS: Record<string, { endpoint: string; protocol: string; headerKey: string; headerValueHint: string }> = {
4+
newrelic: {
5+
endpoint: 'https://otlp.nr-data.net:4318/v1/traces',
6+
protocol: 'http/protobuf',
7+
headerKey: 'api-key',
8+
headerValueHint: 'your-new-relic-ingest-key'
9+
},
10+
datadog: {
11+
endpoint: 'http://<datadog-agent-host>:4318/v1/traces',
12+
protocol: 'http/protobuf',
13+
headerKey: 'dd-api-key',
14+
headerValueHint: 'your-datadog-api-key'
15+
},
16+
logicmonitor: {
17+
endpoint: 'https://<account>.logicmonitor.com/rest/api/v1/traces',
18+
protocol: 'http/protobuf',
19+
headerKey: 'Authorization',
20+
headerValueHint: 'Bearer <token>'
21+
},
22+
grafana: {
23+
endpoint: 'https://otlp-gateway-<region>.grafana.net/otlp/v1/traces',
24+
protocol: 'http/protobuf',
25+
headerKey: 'Authorization',
26+
headerValueHint: 'Basic <base64(instanceId:apiToken)>'
27+
}
28+
}
29+
30+
class OpenTelemetryApi implements INodeCredential {
31+
label: string
32+
name: string
33+
version: number
34+
description: string
35+
inputs: INodeParams[]
36+
37+
constructor() {
38+
this.label = 'OpenTelemetry API'
39+
this.name = 'openTelemetryApi'
40+
this.version = 1.0
41+
this.description =
42+
'Export telemetry data to OpenTelemetry compatible backends. ' +
43+
'Select a Vendor Preset to auto-fill recommended defaults, or choose Custom for any OTLP-compliant endpoint.'
44+
this.inputs = [
45+
{
46+
label: 'Vendor Preset',
47+
name: 'otelVendorPreset',
48+
type: 'options',
49+
options: [
50+
{ label: 'Custom', name: 'custom' },
51+
{ label: 'New Relic', name: 'newrelic' },
52+
{ label: 'Datadog', name: 'datadog' },
53+
{ label: 'LogicMonitor', name: 'logicmonitor' },
54+
{ label: 'Grafana Cloud', name: 'grafana' }
55+
],
56+
default: 'custom',
57+
...({
58+
autoPopulate: {
59+
newrelic: {
60+
otelEndpoint: VENDOR_PRESETS.newrelic.endpoint,
61+
otelProtocol: VENDOR_PRESETS.newrelic.protocol,
62+
otelHeaderKey: VENDOR_PRESETS.newrelic.headerKey,
63+
otelHeaderValue: VENDOR_PRESETS.newrelic.headerValueHint
64+
},
65+
datadog: {
66+
otelEndpoint: VENDOR_PRESETS.datadog.endpoint,
67+
otelProtocol: VENDOR_PRESETS.datadog.protocol,
68+
otelHeaderKey: VENDOR_PRESETS.datadog.headerKey,
69+
otelHeaderValue: VENDOR_PRESETS.datadog.headerValueHint
70+
},
71+
logicmonitor: {
72+
otelEndpoint: VENDOR_PRESETS.logicmonitor.endpoint,
73+
otelProtocol: VENDOR_PRESETS.logicmonitor.protocol,
74+
otelHeaderKey: VENDOR_PRESETS.logicmonitor.headerKey,
75+
otelHeaderValue: VENDOR_PRESETS.logicmonitor.headerValueHint
76+
},
77+
grafana: {
78+
otelEndpoint: VENDOR_PRESETS.grafana.endpoint,
79+
otelProtocol: VENDOR_PRESETS.grafana.protocol,
80+
otelHeaderKey: VENDOR_PRESETS.grafana.headerKey,
81+
otelHeaderValue: VENDOR_PRESETS.grafana.headerValueHint
82+
}
83+
}
84+
} as any)
85+
},
86+
{
87+
label: 'Endpoint',
88+
name: 'otelEndpoint',
89+
type: 'string',
90+
placeholder: 'https://otlp.nr-data.net:4318/v1/traces'
91+
},
92+
{
93+
label: 'Protocol',
94+
name: 'otelProtocol',
95+
type: 'options',
96+
options: [
97+
{
98+
label: 'HTTP/Protobuf',
99+
name: 'http/protobuf'
100+
},
101+
{
102+
label: 'gRPC',
103+
name: 'grpc'
104+
}
105+
],
106+
default: 'http/protobuf'
107+
},
108+
{
109+
label: 'Header Key',
110+
name: 'otelHeaderKey',
111+
type: 'string',
112+
optional: true,
113+
placeholder: 'api-key',
114+
description: 'The header name for authentication (e.g. api-key, Authorization, dd-api-key).'
115+
},
116+
{
117+
label: 'Header Value',
118+
name: 'otelHeaderValue',
119+
type: 'password',
120+
optional: true,
121+
placeholder: 'your-api-key-value',
122+
description: 'The header value (typically an API key or token). Masked in the UI and encrypted before storage.'
123+
},
124+
{
125+
label: 'Service Name',
126+
name: 'otelServiceName',
127+
type: 'string',
128+
optional: true,
129+
default: 'flowise'
130+
},
131+
{
132+
label: 'Environment',
133+
name: 'otelEnvironment',
134+
type: 'string',
135+
optional: true,
136+
additionalParams: true,
137+
default: 'production'
138+
},
139+
{
140+
label: 'Sampling Rate',
141+
name: 'otelSamplingRate',
142+
type: 'number',
143+
optional: true,
144+
additionalParams: true,
145+
default: 1.0,
146+
description: 'Value between 0.0 and 1.0, where 1.0 means 100% of traces are sampled.'
147+
},
148+
{
149+
label: 'Max Queue Size',
150+
name: 'otelMaxQueueSize',
151+
type: 'number',
152+
optional: true,
153+
additionalParams: true,
154+
default: 2048,
155+
description: 'Maximum number of spans in the export queue.'
156+
},
157+
{
158+
label: 'Schedule Delay (ms)',
159+
name: 'otelScheduleDelayMs',
160+
type: 'number',
161+
optional: true,
162+
additionalParams: true,
163+
default: 5000,
164+
description: 'Delay interval in milliseconds between two consecutive exports.'
165+
},
166+
{
167+
label: 'Max Export Batch Size',
168+
name: 'otelMaxExportBatchSize',
169+
type: 'number',
170+
optional: true,
171+
additionalParams: true,
172+
default: 512,
173+
description: 'Maximum number of spans exported in a single batch.'
174+
},
175+
{
176+
label: 'Export Timeout (ms)',
177+
name: 'otelExportTimeoutMs',
178+
type: 'number',
179+
optional: true,
180+
additionalParams: true,
181+
default: 30000,
182+
description: 'How long the export can run before it is cancelled.'
183+
},
184+
{
185+
label: 'TLS Insecure',
186+
name: 'otelTlsInsecure',
187+
type: 'boolean',
188+
optional: true,
189+
additionalParams: true,
190+
default: false,
191+
description: 'Bypass TLS certificate validation (for development only, not recommended for production).'
192+
}
193+
]
194+
}
195+
}
196+
197+
module.exports = { credClass: OpenTelemetryApi }

packages/components/jest.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module.exports = {
22
preset: 'ts-jest',
33
testEnvironment: 'node',
4-
roots: ['<rootDir>/nodes', '<rootDir>/src'],
4+
roots: ['<rootDir>/nodes', '<rootDir>/src', '<rootDir>/test'],
55
transform: {
66
'^.+\\.tsx?$': 'ts-jest'
77
},
@@ -10,6 +10,7 @@ module.exports = {
1010
verbose: true,
1111
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
1212
moduleNameMapper: {
13+
'^uuid$': require.resolve('uuid'),
1314
'^../../../src/(.*)$': '<rootDir>/src/$1',
1415
// @modelcontextprotocol/sdk is ESM-only (type:module, no exports map, no CJS builds).
1516
// It cannot be require()'d in Jest's CJS environment and crashes the worker.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { INode, INodeParams } from '../../../src/Interface'
2+
3+
class OpenTelemetry_Analytic implements INode {
4+
label: string
5+
name: string
6+
version: number
7+
description: string
8+
type: string
9+
icon: string
10+
category: string
11+
baseClasses: string[]
12+
inputs?: INodeParams[]
13+
credential: INodeParams
14+
15+
constructor() {
16+
this.label = 'OpenTelemetry'
17+
this.name = 'openTelemetry'
18+
this.version = 1.0
19+
this.type = 'OpenTelemetry'
20+
this.icon = 'otel.svg'
21+
this.category = 'Analytic'
22+
this.baseClasses = [this.type]
23+
this.inputs = []
24+
this.credential = {
25+
label: 'Connect Credential',
26+
name: 'credential',
27+
type: 'credential',
28+
credentialNames: ['openTelemetryApi']
29+
}
30+
}
31+
}
32+
33+
module.exports = { nodeClass: OpenTelemetry_Analytic }
Lines changed: 4 additions & 0 deletions
Loading

packages/components/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"@opentelemetry/api": "1.9.0",
8888
"@opentelemetry/auto-instrumentations-node": "^0.52.0",
8989
"@opentelemetry/core": "1.27.0",
90+
"@opentelemetry/instrumentation": "0.54.2",
9091
"@opentelemetry/exporter-metrics-otlp-grpc": "0.54.0",
9192
"@opentelemetry/exporter-metrics-otlp-http": "0.54.0",
9293
"@opentelemetry/exporter-metrics-otlp-proto": "0.54.0",
@@ -97,6 +98,7 @@
9798
"@opentelemetry/sdk-metrics": "1.27.0",
9899
"@opentelemetry/sdk-node": "^0.54.0",
99100
"@opentelemetry/sdk-trace-base": "1.27.0",
101+
"@opentelemetry/sdk-trace-node": "1.27.0",
100102
"@opentelemetry/semantic-conventions": "1.27.0",
101103
"@pinecone-database/pinecone": "4.0.0",
102104
"@qdrant/js-client-rest": "^1.17.0",
@@ -127,6 +129,7 @@
127129
"form-data": "^4.0.4",
128130
"google-auth-library": "^9.4.0",
129131
"graphql": "^16.6.0",
132+
"groq-sdk": "^0.5.0",
130133
"html-to-text": "^9.0.5",
131134
"ioredis": "^5.3.2",
132135
"ipaddr.js": "^2.2.0",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { ICommonObject } from '../../Interface'
2+
import { OtelDestinationConfig, OtelDestinationConfigSchema } from './OtelConfigSchema'
3+
import { OtelTracerProviderPool } from './OtelTracerProviderPool'
4+
import { OtelLangChainCallbackHandler } from './OtelLangChainCallbackHandler'
5+
6+
interface OtelAnalyticsOptions {
7+
chatId?: string
8+
spanAttributes?: Record<string, string>
9+
overrideConfig?: ICommonObject
10+
}
11+
12+
/**
13+
* Builds the destination config from credential data, applying Zod defaults.
14+
*/
15+
export function buildDestinationConfig(credentialData: ICommonObject): OtelDestinationConfig {
16+
let headers: Record<string, string> | undefined
17+
18+
if (credentialData.otelHeaderKey && credentialData.otelHeaderValue) {
19+
headers = { [credentialData.otelHeaderKey]: credentialData.otelHeaderValue }
20+
} else if (credentialData.otelHeaders) {
21+
try {
22+
headers = typeof credentialData.otelHeaders === 'string' ? JSON.parse(credentialData.otelHeaders) : credentialData.otelHeaders
23+
} catch {
24+
headers = undefined
25+
}
26+
}
27+
28+
return OtelDestinationConfigSchema.parse({
29+
protocol: credentialData.otelProtocol ?? 'http/protobuf',
30+
endpoint: credentialData.otelEndpoint,
31+
headers,
32+
serviceName: credentialData.otelServiceName || undefined,
33+
environment: credentialData.otelEnvironment || undefined,
34+
samplingRate: credentialData.otelSamplingRate != null ? Number(credentialData.otelSamplingRate) : undefined,
35+
maxQueueSize: credentialData.otelMaxQueueSize != null ? Number(credentialData.otelMaxQueueSize) : undefined,
36+
scheduleDelayMs: credentialData.otelScheduleDelayMs != null ? Number(credentialData.otelScheduleDelayMs) : undefined,
37+
maxExportBatchSize: credentialData.otelMaxExportBatchSize != null ? Number(credentialData.otelMaxExportBatchSize) : undefined,
38+
exportTimeoutMs: credentialData.otelExportTimeoutMs != null ? Number(credentialData.otelExportTimeoutMs) : undefined,
39+
tlsInsecure: credentialData.otelTlsInsecure === true || credentialData.otelTlsInsecure === 'true'
40+
})
41+
}
42+
43+
export async function getCallbackHandler(
44+
chatflowId: string,
45+
otelConfig: ICommonObject,
46+
credentialData: ICommonObject,
47+
options: OtelAnalyticsOptions
48+
): Promise<OtelLangChainCallbackHandler> {
49+
const destConfig = buildDestinationConfig(credentialData)
50+
const pool = OtelTracerProviderPool.getInstance()
51+
const tracer = await pool.getOrCreate(chatflowId, destConfig)
52+
53+
return new OtelLangChainCallbackHandler({
54+
tracer,
55+
chatflowId,
56+
chatId: options.chatId,
57+
spanAttributes: otelConfig.spanAttributes ?? options.spanAttributes,
58+
overrideConfig: options.overrideConfig
59+
})
60+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { z } from 'zod'
2+
3+
export const OtelDestinationConfigSchema = z.object({
4+
id: z.string().optional(),
5+
label: z.string().optional(),
6+
enabled: z.boolean().default(true),
7+
protocol: z.enum(['http/protobuf', 'grpc']).default('http/protobuf'),
8+
endpoint: z
9+
.string()
10+
.url('Endpoint must be a valid URL')
11+
.refine(
12+
(url) => {
13+
try {
14+
const parsed = new URL(url)
15+
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
16+
} catch {
17+
return false
18+
}
19+
},
20+
{ message: 'Endpoint must use http:// or https:// protocol' }
21+
),
22+
headers: z.record(z.string(), z.string()).optional(),
23+
serviceName: z.string().default('flowise'),
24+
environment: z.string().default('production'),
25+
samplingRate: z.number().min(0).max(1).default(1.0),
26+
maxQueueSize: z.number().int().positive().default(2048),
27+
scheduleDelayMs: z.number().int().positive().default(5000),
28+
maxExportBatchSize: z.number().int().positive().default(512),
29+
exportTimeoutMs: z.number().int().positive().default(30000),
30+
tlsInsecure: z.boolean().default(false)
31+
})
32+
33+
export type OtelDestinationConfig = z.infer<typeof OtelDestinationConfigSchema>

0 commit comments

Comments
 (0)