Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(instrumentation): add fw metrics #187

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BEE_FRAMEWORK_INSTRUMENTATION_METRICS_ENABLED=false
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ This project and everyone participating in it are governed by the [Code of Condu

All content in these repositories including code has been provided by IBM under the associated open source software license and IBM is under no obligation to provide enhancements, updates, or support. IBM developers produced this code as an open source project (not as an IBM product), and IBM makes no assertions as to the level of quality nor security, and will not be maintaining this code going forward.

## Telemetry

Some metrics are collected by default. See the [Native Telemetry](/docs/native-telemetry.md) for more detail.

## Contributors

Special thanks to our contributors for helping us improve Bee Agent Framework.
Expand Down
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ This project and everyone participating in it are governed by the [Code of Condu

All content in these repositories including code has been provided by IBM under the associated open source software license and IBM is under no obligation to provide enhancements, updates, or support. IBM developers produced this code as an open source project (not as an IBM product), and IBM makes no assertions as to the level of quality nor security, and will not be maintaining this code going forward.

## Telemetry

Some metrics are collected by default. See the [Native Telemetry](/docs/native-telemetry.md) for more detail.

## Contributors

Special thanks to our contributors for helping us improve Bee Agent Framework.
Expand Down
4 changes: 2 additions & 2 deletions docs/instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ You can manually create spans during the `run` process to track specific parts o
Example of creating a span:

```ts
import { trace } from "@opentelemetry/api";
import { api } from "@opentelemetry/sdk-node";

const tracer = trace.getTracer("bee-agent-framework");
const tracer = api.trace.getTracer("bee-agent-framework");

function exampleFunction() {
const span = tracer.startSpan("example-function-span");
Expand Down
26 changes: 26 additions & 0 deletions docs/native-telemetry.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding this document to the documentation sidebar. (docs/_sidebar.md)

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Native Telemetry in Bee Agent Framework

The Bee Agent Framework comes with built-in telemetry capabilities to help users monitor and optimize their applications. This document provides an overview of the telemetry feature, including what data is collected and how to disable it if necessary.

## Overview

The telemetry functionality in the Bee Agent Framework collects performance metrics and operational data to provide insights into how the framework operates in real-world environments. This feature helps us:

- Identify performance bottlenecks.
- Improve framework stability and reliability.
- Enhance user experience by understanding usage patterns.

## Data Collected

We value your privacy and ensure that **no sensitive data** is collected through telemetry. The following types of information are gathered:

- Framework version and runtime environment details.
- Anonymized usage statistics for built-in features.

## Disabling Telemetry

We understand that not all users want to send telemetry data. You can easily disable this feature by setting an environment variable:

```bash
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't there support for ```env ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't seen the ```env yet, I will remove the bash part and the markdown will highlight it correctly.

BEE_FRAMEWORK_INSTRUMENTATION_METRICS_ENABLED=false
```
5 changes: 2 additions & 3 deletions examples/helpers/telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import "@opentelemetry/instrumentation/hook.mjs";
import { NodeSDK } from "@opentelemetry/sdk-node";
import { NodeSDK, resources } from "@opentelemetry/sdk-node";
import { ConsoleSpanExporter } from "@opentelemetry/sdk-trace-node";
import { Resource } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";

const sdk = new NodeSDK({
resource: new Resource({
resource: new resources.Resource({
[ATTR_SERVICE_NAME]: "bee-agent-framework",
[ATTR_SERVICE_VERSION]: "0.0.1",
}),
Expand Down
5 changes: 0 additions & 5 deletions examples/llms/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,3 @@ const response = await llm.generate([
]);

logger.info(`LLM 🤖 (txt) : ${response.getTextContent()}`);

// Wait briefly to ensure all telemetry data has been processed
setTimeout(() => {
logger.info("Process exiting after OpenTelemetry flush.");
}, 5_000); // Adjust the delay as needed
5 changes: 0 additions & 5 deletions examples/tools/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,3 @@ const result = await tool.run({
end_date: "2024-10-10",
});
logger.info(`OpenMeteoTool 🤖 (txt) : ${result.getTextContent()}`);

// Wait briefly to ensure all telemetry data has been processed
setTimeout(() => {
logger.info("Process exiting after OpenTelemetry flush.");
}, 5_000); // Adjust the delay as needed
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@
"@ai-zen/node-fetch-event-source": "^2.1.4",
"@connectrpc/connect": "^1.6.1",
"@connectrpc/connect-node": "^1.6.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.54.2",
"@opentelemetry/sdk-node": "^0.54.2",
"@opentelemetry/semantic-conventions": "^1.27.0",
"@streamparser/json": "^0.0.21",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
Expand Down Expand Up @@ -262,10 +264,7 @@
"@langchain/community": "~0.3.12",
"@langchain/core": "~0.3.17",
"@opentelemetry/instrumentation": "^0.54.0",
"@opentelemetry/resources": "^1.27.0",
"@opentelemetry/sdk-node": "^0.54.0",
"@opentelemetry/sdk-trace-node": "^1.27.0",
"@opentelemetry/semantic-conventions": "^1.27.0",
"@release-it/conventional-changelog": "^8.0.2",
"@rollup/plugin-commonjs": "^28.0.1",
"@swc/core": "^1.7.36",
Expand Down
12 changes: 10 additions & 2 deletions src/agents/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ import { GetRunContext, RunContext } from "@/context.js";
import { Emitter } from "@/emitter/emitter.js";
import { BaseMemory } from "@/memory/base.js";
import { createTelemetryMiddleware } from "@/instrumentation/create-telemetry-middleware.js";
import { INSTRUMENTATION_ENABLED } from "@/instrumentation/config.js";
import {
INSTRUMENTATION_ENABLED,
INSTRUMENTATION_METRICS_ENABLED,
} from "@/instrumentation/config.js";
import { doNothing } from "remeda";
import { createTelemetryMetricsMiddleware } from "@/instrumentation/create-telemetry-metrics-middleware.js";

export class AgentError extends FrameworkError {}

Expand Down Expand Up @@ -65,7 +69,11 @@ export abstract class BaseAgent<
this.isRunning = false;
}
},
).middleware(INSTRUMENTATION_ENABLED ? createTelemetryMiddleware() : doNothing());
)
.middleware(INSTRUMENTATION_ENABLED ? createTelemetryMiddleware() : doNothing())
.middleware(
INSTRUMENTATION_METRICS_ENABLED ? createTelemetryMetricsMiddleware() : doNothing(),
);
}

protected abstract _run(
Expand Down
4 changes: 4 additions & 0 deletions src/instrumentation/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import { parseEnv } from "@/internals/env.js";
import { z } from "zod";

export const INSTRUMENTATION_ENABLED = parseEnv.asBoolean("BEE_FRAMEWORK_INSTRUMENTATION_ENABLED");
export const INSTRUMENTATION_METRICS_ENABLED = parseEnv.asBoolean(
"BEE_FRAMEWORK_INSTRUMENTATION_METRICS_ENABLED",
true,
);

export const INSTRUMENTATION_IGNORED_KEYS = parseEnv(
"BEE_FRAMEWORK_INSTRUMENTATION_IGNORED_KEYS",
Expand Down
62 changes: 62 additions & 0 deletions src/instrumentation/create-telemetry-metrics-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Copyright 2024 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { GetRunContext, RunInstance } from "@/context.js";
import { FrameworkError } from "@/errors.js";
import { buildModuleUsageMetric, isMeasurementedInstance } from "./opentelemetry.js";
import { GenerateCallbacks } from "@/llms/base.js";
import { createFullPath } from "@/emitter/utils.js";
import { metricReader } from "./sdk.js";

export const activeTracesMap = new Map<string, string>();
Tomas2D marked this conversation as resolved.
Show resolved Hide resolved

/**
* This middleware collects the usage metrics from framework entities runs and sends them to the central collector
* @returns
*/
export function createTelemetryMetricsMiddleware() {
return (context: GetRunContext<RunInstance, unknown>) => {
const traceId = context.emitter?.trace?.id;
if (!traceId) {
throw new FrameworkError(`Fatal error. Missing traceId`, [], { context });
}
if (activeTracesMap.has(traceId)) {
return;
}
activeTracesMap.set(traceId, context.instance.constructor.name);

const { emitter } = context;
const basePath = createFullPath(emitter.namespace, "");

const startEventName: keyof GenerateCallbacks = `start`;
const finishEventName: keyof GenerateCallbacks = `finish`;

// collect module_usage metric for llm|tool|agent start event
emitter.match(
(event) => event.name === startEventName && isMeasurementedInstance(event.creator),
(_, meta) => buildModuleUsageMetric({ traceId, instance: meta.creator, eventId: meta.id }),
);

// send metrics to the public collector
emitter.match(
(event) => event.path === `${basePath}.run.${finishEventName}`,
async () => {
activeTracesMap.delete(traceId);
await metricReader.forceFlush();
},
);
};
}
4 changes: 3 additions & 1 deletion src/instrumentation/create-telemetry-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { Version } from "@/version.js";
import { Role } from "@/llms/primitives/message.js";
import type { GetRunContext, RunInstance } from "@/context.js";
import type { GeneratedResponse, FrameworkSpan } from "./types.js";
import { activeTracesMap, buildTraceTree } from "./tracer.js";
import { buildTraceTree } from "./opentelemetry.js";
import { traceSerializer } from "./helpers/trace-serializer.js";
import { INSTRUMENTATION_IGNORED_KEYS } from "./config.js";
import { createFullPath } from "@/emitter/utils.js";
Expand All @@ -36,6 +36,8 @@ import { instrumentationLogger } from "./logger.js";
import { BaseAgent } from "@/agents/base.js";
import { assertLLMWithMessagesToPromptFn } from "./helpers/utils.js";

export const activeTracesMap = new Map<string, string>();

export function createTelemetryMiddleware() {
return (context: GetRunContext<RunInstance, unknown>) => {
if (!context.emitter?.trace?.id) {
Expand Down
4 changes: 2 additions & 2 deletions src/instrumentation/helpers/create-span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { SpanStatusCode } from "@opentelemetry/api";
import { api } from "@opentelemetry/sdk-node";
import { FrameworkSpan } from "@/instrumentation/types.js";
import { isEmpty } from "remeda";

Expand Down Expand Up @@ -51,7 +51,7 @@ export function createSpan({
},
parent_id: parent?.id,
status: {
code: error ? SpanStatusCode.ERROR : SpanStatusCode.OK,
code: error ? api.SpanStatusCode.ERROR : api.SpanStatusCode.OK,
message: error ? error : "",
},
start_time: startedAt,
Expand Down
106 changes: 106 additions & 0 deletions src/instrumentation/opentelemetry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* Copyright 2024 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { BeeAgent } from "@/agents/bee/agent.js";
import { ElasticSearchTool } from "@/tools/database/elasticsearch.js";
import { SQLTool } from "@/tools/database/sql.js";
import { GoogleSearchTool } from "@/tools/search/googleSearch.js";
import { OpenMeteoTool } from "@/tools/weather/openMeteo.js";
import { expect, describe } from "vitest";
import { isMeasurementedInstance } from "./opentelemetry.js";
import { DuckDuckGoSearchTool } from "@/tools/search/duckDuckGoSearch.js";
import { WebCrawlerTool } from "@/tools/web/webCrawler.js";
import { ArXivTool } from "@/tools/arxiv.js";
import { CalculatorTool } from "@/tools/calculator.js";
import { LLMTool } from "@/tools/llm.js";
import { OllamaLLM } from "@/adapters/ollama/llm.js";
import { WatsonXLLM } from "@/adapters/watsonx/llm.js";
import { LLM } from "@/llms/llm.js";
import { Emitter } from "@/emitter/emitter.js";
import { GenerateCallbacks, LLMMeta, BaseLLMTokenizeOutput, AsyncStream } from "@/llms/base.js";
import { OllamaChatLLM } from "@/adapters/ollama/chat.js";
import { TokenMemory } from "@/memory/tokenMemory.js";
import { GraniteBeeAgent } from "@/agents/granite/agent.js";
import { SlidingCache } from "@/cache/slidingCache.js";

export class CustomLLM extends LLM<any> {
public readonly emitter = Emitter.root.child<GenerateCallbacks>({
namespace: ["bam", "llm"],
creator: this,
});
meta(): Promise<LLMMeta> {
throw new Error("Method not implemented.");
}
tokenize(): Promise<BaseLLMTokenizeOutput> {
throw new Error("Method not implemented.");
}
protected _generate(): Promise<any> {
throw new Error("Method not implemented.");
}
protected _stream(): AsyncStream<any, void> {
throw new Error("Method not implemented.");
}
}

const llm = new OllamaChatLLM({ modelId: "llama3.1" });
const memory = new TokenMemory({ llm });

describe("opentelemetry", () => {
describe("isMeasurementedInstance", () => {
it.each([
// tool
new OpenMeteoTool(),
new GoogleSearchTool({ apiKey: "xx", cseId: "xx", maxResults: 10 }),
new ElasticSearchTool({ connection: { cloud: { id: "" } } }),
new SQLTool({ connection: { dialect: "mariadb" }, provider: "mysql" }),
new DuckDuckGoSearchTool(),
new OpenMeteoTool(),
new WebCrawlerTool(),
new ArXivTool(),
new CalculatorTool(),
new LLMTool({ llm: new OllamaLLM({ modelId: "llama3.1" }) }),
// llm
new OllamaLLM({ modelId: "llama3.1" }),
new WatsonXLLM({ modelId: "llama3.1", apiKey: "xx" }),
new CustomLLM("llama3.1"),
new OllamaChatLLM({ modelId: "llama3.1" }),
// agent
new BeeAgent({ llm, memory, tools: [] }),
new GraniteBeeAgent({ llm, memory, tools: [] }),
])("Should return true for '%s'", (value) => {
expect(isMeasurementedInstance(value)).toBeTruthy();
});

it.each([
null,
undefined,
"",
0,
"string",
{},
memory,
new SlidingCache({
size: 50,
}),
new Emitter({
namespace: ["app"],
creator: this,
}),
])("Should return false for '%s'", (value) => {
expect(isMeasurementedInstance(value)).toBeFalsy();
});
});
});
Loading