diff --git a/.env.example b/.env.example index dac5d07..f30f08c 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,52 @@ -# Enable debug logging +# PostHog Configuration (existing) +POSTHOG_API_KEY=your_posthog_api_key_here +POSTHOG_HOST=https://app.posthog.com + +# OpenTelemetry Configuration +# ========================== + +# Service Configuration +OTEL_SERVICE_NAME=llm-observability-mcp +OTEL_SERVICE_VERSION=1.0.0 +OTEL_ENVIRONMENT=development + +# OTLP Endpoints +# Uncomment and configure based on your backend + +# Jaeger (local development) +# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 + +# New Relic +# OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4318 +# OTEL_EXPORTER_OTLP_HEADERS=api-key=YOUR_NEW_RELIC_LICENSE_KEY + +# Grafana Cloud +# OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp +# OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic YOUR_BASE64_ENCODED_CREDENTIALS + +# Datadog +# OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com/api/v2/series +# OTEL_EXPORTER_OTLP_HEADERS=DD-API-KEY=YOUR_DD_API_KEY + +# Honeycomb +# OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io/v1/traces +# OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=YOUR_API_KEY + +# Lightstep +# OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.lightstep.com:443/api/v2/otel/trace +# OTEL_EXPORTER_OTLP_HEADERS=lightstep-access-token=YOUR_ACCESS_TOKEN + +# Separate endpoints (optional) +# OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://localhost:4318/v1/metrics +# OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces +# OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://localhost:4318/v1/logs + +# Export Configuration +OTEL_METRIC_EXPORT_INTERVAL=10000 +OTEL_METRIC_EXPORT_TIMEOUT=5000 + +# Sampling Configuration +OTEL_TRACES_SAMPLER_ARG=1.0 + +# Debug Mode DEBUG=false - -# Transport mode -TRANSPORT_MODE= - -# PostHog LLM Observability configuration -POSTHOG_API_KEY= -POSTHOG_HOST=https://us.i.posthog.com diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index a926754..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 10 - versioning-strategy: auto - labels: - - "dependencies" - commit-message: - prefix: "chore" - include: "scope" - allow: - - dependency-type: "direct" - ignore: - - dependency-name: "*" - update-types: ["version-update:semver-patch"] - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 5 - labels: - - "dependencies" - - "github-actions" \ No newline at end of file diff --git a/.github/workflows/ci-dependabot-auto-merge.yml b/.github/workflows/ci-dependabot-auto-merge.yml deleted file mode 100644 index fd32a18..0000000 --- a/.github/workflows/ci-dependabot-auto-merge.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: CI - Dependabot Auto-merge - -on: - pull_request: - branches: [main] - -permissions: - contents: write - pull-requests: write - checks: read - -jobs: - auto-merge-dependabot: - runs-on: ubuntu-latest - if: github.actor == 'dependabot[bot]' - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm test - - - name: Run linting - run: npm run lint - - - name: Auto-approve PR - uses: hmarr/auto-approve-action@v4 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Enable auto-merge - if: success() - run: gh pr merge --auto --merge "$PR_URL" - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci-dependency-check.yml b/.github/workflows/ci-dependency-check.yml deleted file mode 100644 index 9bc7f19..0000000 --- a/.github/workflows/ci-dependency-check.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: CI - Dependency Check - -on: - schedule: - - cron: '0 5 * * 1' # Run at 5 AM UTC every Monday - workflow_dispatch: # Allow manual triggering - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run npm audit - run: npm audit - - - name: Check for outdated dependencies - run: npm outdated || true - - - name: Run tests - run: npm test - - - name: Run linting - run: npm run lint - - - name: Build project - run: npm run build diff --git a/.github/workflows/ci-semantic-release.yml b/.github/workflows/ci-semantic-release.yml deleted file mode 100644 index 4519894..0000000 --- a/.github/workflows/ci-semantic-release.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: CI - Semantic Release - -# This workflow is triggered on every push to the main branch -# It analyzes commits and automatically releases a new version when needed -on: - push: - branches: [main] - -jobs: - release: - name: Semantic Release - runs-on: ubuntu-latest - # Permissions needed for creating releases, updating issues, and publishing packages - permissions: - contents: write # Needed to create releases and tags - issues: write # Needed to comment on issues - pull-requests: write # Needed to comment on pull requests - # packages permission removed as we're not using GitHub Packages - steps: - # Step 1: Check out the full Git history for proper versioning - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetches all history for all branches and tags - - # Step 2: Setup Node.js environment - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 # Using Node.js 22 - cache: 'npm' # Enable npm caching - - # Step 3: Install dependencies with clean install - - name: Install dependencies - run: npm ci # Clean install preserving package-lock.json - - # Step 4: Build the package - - name: Build - run: npm run build # Compiles TypeScript to JavaScript - - # Step 5: Ensure executable permissions - - name: Set executable permissions - run: chmod +x dist/index.js - - # Step 6: Run tests to ensure quality - - name: Verify tests - run: npm test # Runs Jest tests - - # Step 7: Configure Git identity for releases - - name: Configure Git User - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - - # Step 8: Run semantic-release to analyze commits and publish to npm - - name: Semantic Release - id: semantic - env: - # Tokens needed for GitHub and npm authentication - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For creating releases and commenting - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # For publishing to npm - run: | - echo "Running semantic-release for version bump and npm publishing" - npx semantic-release - - # Note: GitHub Packages publishing has been removed diff --git a/README.md b/README.md index 69133d2..a6a26c9 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,24 @@ -# LLM Observability MCP for PostHog +# LLM Observability MCP Server [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -A Model Context Protocol (MCP) server that provides a tool to capture LLM Observability events and send them to PostHog. +A Model Context Protocol (MCP) server that provides comprehensive LLM observability tools supporting both PostHog and OpenTelemetry backends. ## Overview -This project is an MCP server designed to track and observe Large Language Model (LLM) interactions using [PostHog's LLM Observability](https://posthog.com/docs/llm-observability) features. It allows you to capture detailed information about LLM requests, responses, performance, and costs, providing valuable insights into your AI-powered applications. +This project is an MCP server designed to track and observe Large Language Model (LLM) interactions using both [PostHog's LLM Observability](https://posthog.com/docs/llm-observability) and **OpenTelemetry** for universal observability across any backend that supports OpenTelemetry (Jaeger, New Relic, Grafana, Datadog, Honeycomb, etc.). The server can be run as a local process communicating over `stdio` or as a remote `http` server, making it compatible with any MCP client, such as AI-powered IDEs (e.g., VS Code with an MCP extension, Cursor) or custom applications. ## Features -- **Capture LLM Metrics**: Log key details of LLM interactions, including model, provider, latency, token counts, and more. -- **Flexible Transport**: Run as a local `stdio` process for tight IDE integration or as a standalone `http` server for remote access. -- **Dynamic Configuration**: Configure the server easily using environment variables. -- **Easy Integration**: Connect to MCP-compatible IDEs or use the programmatic client for use in any TypeScript/JavaScript application. +- **Dual Backend Support**: Use PostHog, OpenTelemetry, or both in parallel +- **Universal OpenTelemetry**: Works with any OpenTelemetry-compatible backend +- **Comprehensive Metrics**: Request counts, token usage, latency, error rates +- **Distributed Tracing**: Full request lifecycle tracking with spans +- **Flexible Transport**: Run as local `stdio` process or standalone `http` server +- **Dynamic Configuration**: Environment-based configuration for different backends +- **Zero-Code Integration**: Easy integration with MCP-compatible clients ## Installation for Development @@ -23,7 +26,7 @@ Follow these steps to set up the server for local development. 1. **Prerequisites**: - Node.js (>=18.x) - - A [PostHog account](https://posthog.com/) with an API Key and Host URL. + - A [PostHog account](https://posthog.com/) with an API Key and Host URL (if using PostHog). 2. **Clone and Install**: @@ -40,16 +43,37 @@ Follow these steps to set up the server for local development. cp .env.example .env ``` - Then, edit the `.env` file with your PostHog credentials and desired transport mode. + Then, edit the `.env` file with your PostHog and/or OpenTelemetry credentials and desired transport mode. ## Configuration -The server is configured via environment variables. +The server is configured via environment variables. See `.env.example` for all options. + +### PostHog Configuration + +| Variable | Description | Default | Example | +| ----------------- | --------------------------------------------------------------------------- | --------- | ------------------------------------- | +| `POSTHOG_API_KEY` | Your PostHog Project API Key (required for PostHog tool) | - | `phc_...` | +| `POSTHOG_HOST` | The URL of your PostHog instance | - | `https://us.i.posthog.com` | + +### OpenTelemetry Configuration + +See [OpenTelemetry Documentation](docs/opentelemetry.md) for full details and backend-specific setup. + +| Variable | Description | Default | Example | +| ------------------------------- | --------------------------------------------------------------------------- | -------------------------- | ------------------------------------- | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | OpenTelemetry collector endpoint | - | `http://localhost:4318` | +| `OTEL_EXPORTER_OTLP_HEADERS` | Headers for authentication (comma-separated key=value pairs) | - | `api-key=YOUR_KEY` | +| `OTEL_SERVICE_NAME` | Service name for traces and metrics | `llm-observability-mcp` | `my-llm-app` | +| `OTEL_SERVICE_VERSION` | Service version | `1.0.0` | `2.1.0` | +| `OTEL_ENVIRONMENT` | Environment name | `development` | `production` | +| `OTEL_TRACES_SAMPLER_ARG` | Sampling ratio (0.0-1.0) | `1.0` | `0.1` | +| `OTEL_METRIC_EXPORT_INTERVAL` | Metrics export interval in milliseconds | `10000` | `30000` | + +### General Configuration | Variable | Description | Default | Example | | ----------------- | --------------------------------------------------------------------------- | --------- | ------------------------------------- | -| `POSTHOG_API_KEY` | **Required.** Your PostHog Project API Key. | - | `phc_...` | -| `POSTHOG_HOST` | **Required.** The URL of your PostHog instance. | - | `https://us.i.posthog.com` | | `TRANSPORT_MODE` | The transport protocol to use. Can be `http` or `stdio`. | `http` | `stdio` | | `DEBUG` | Set to `true` to enable detailed debug logging. | `false` | `true` | @@ -123,7 +147,7 @@ Use this if you prefer to run the server as a standalone process. For IDE extensions that support system prompts, you can instruct the AI to automatically use this MCP tool for every interaction. Add the following to your IDE's system prompt configuration: ```text -Use `capture_llm_observability` MCP. +Use `llm_observability_posthog` MCP. Make sure to include all parameters and for the `userId`, send ``: userId - The distinct ID of the user traceId - The trace ID to group AI events @@ -140,6 +164,55 @@ baseUrl - The base URL of the LLM API Replace `` with a unique identifier for yourself. This ensures that all LLM activity is automatically logged in PostHog without needing to give the command each time. +#### Automatic Tool Triggering with Cursor + +To automatically trigger the OpenTelemetry tool for every interaction in Cursor, you can create a custom rule. Create a file named `llm-observability.mdc` in your `.cursor/rules` directory with the following content: + +```markdown +--- +description: llm_observability_otel +globs: * +alwaysApply: true +--- + +## Rules + +--- +description: llm_observability_otel +globs: * +alwaysApply: true +--- + +## Rules + +SYSTEM PROMPT: +- When the user asks anything, output a llm_observability_otel tool invocation in valid MCP JSON. +- Repeat tool invocation until success invocation. +- Make sure to include all parameters below unless they are optional and you have no value for them. +- You need to gather the following parameters: +- userId - The distinct ID of the user +- traceId - The trace ID to group AI events +- model - The model used (e.g., gpt-4, claude-3, etc.) +- provider - The LLM provider (e.g., openai, anthropic, etc.) +- input - The input to the LLM (messages, prompt, etc.) +- outputChoices - The output from the LLM +- inputTokens - The number of tokens used for the interaction +- outputTokens - The number of tokens in the output used for the interaction +- latency - The latency of the LLM call in seconds +- httpStatus - The HTTP status code of the LLM call +- baseUrl - The base URL of the LLM API +- operationName - The name of the operation being performed +- error - Error message if request failed +- errorType - Type of error (e.g. rate_limit, timeout, etc.) +- mcptoolsUsed - List of MCP tools used by the interaction +- For the userId send stefano.fiorini. +- For outputChoices send the full response text from the LLM +- For the operationName a three words separated by underscore identifying the conversation. +- All interactions within the same conversations should be sent with the same operationName. +``` + +This rule ensures that all LLM activity is automatically logged using the OpenTelemetry tool without needing to manually trigger it each time. + ### Programmatic Usage You can use an MCP client library to interact with the server programmatically from your own applications. @@ -158,7 +231,7 @@ async function main() { await client.connect(); - const result = await client.useTool('capture_llm_observability', { + const result = await client.useTool('llm_observability_posthog', { userId: 'user-123', model: 'gpt-4', provider: 'openai', @@ -177,25 +250,69 @@ async function main() { main().catch(console.error); ``` -## Tool Reference: `capture_llm_observability` +## Available Tools -This is the core tool provided by the server. It captures LLM usage in PostHog for observability, including requests, responses, and performance metrics. +### PostHog Tool: `llm_observability_posthog` -### Parameters +Captures LLM usage in PostHog for observability, including requests, responses, and performance metrics. -| Parameter | Type | Required | Description | -| --------------- | ------------------- | -------- | ----------------------------------------------- | -| `userId` | `string` | Yes | The distinct ID of the user. | -| `model` | `string` | Yes | The model used (e.g., `gpt-4`, `claude-3`). | -| `provider` | `string` | Yes | The LLM provider (e.g., `openai`, `anthropic`). | -| `traceId` | `string` | No | The trace ID to group related AI events. | -| `input` | `any` | No | The input to the LLM (e.g., messages, prompt). | -| `outputChoices` | `any` | No | The output choices from the LLM. | -| `inputTokens` | `number` | No | The number of tokens in the input. | -| `outputTokens` | `number` | No | The number of tokens in the output. | -| `latency` | `number` | No | The latency of the LLM call in seconds. | -| `httpStatus` | `number` | No | The HTTP status code of the LLM API call. | -| `baseUrl` | `string` | No | The base URL of the LLM API. | +#### Parameters for PostHog + +- `userId` (string, required): The distinct ID of the user +- `model` (string, required): The model used (e.g., `gpt-4`, `claude-3`) +- `provider` (string, required): The LLM provider (e.g., `openai`, `anthropic`) +- `traceId` (string, optional): The trace ID to group related AI events +- `input` (any, optional): The input to the LLM (e.g., messages, prompt) +- `outputChoices` (any, optional): The output choices from the LLM +- `inputTokens` (number, optional): The number of tokens in the input +- `outputTokens` (number, optional): The number of tokens in the output +- `latency` (number, optional): The latency of the LLM call in seconds +- `httpStatus` (number, optional): The HTTP status code of the LLM API call +- `baseUrl` (string, optional): The base URL of the LLM API + +### OpenTelemetry Tool: `llm_observability_otel` + +Captures LLM usage using OpenTelemetry for universal observability across any OpenTelemetry-compatible backend. + +See [OpenTelemetry Documentation](docs/opentelemetry.md) for full details, backend setup, advanced usage, and troubleshooting. + +#### Parameters for OpenTelemetry + +- `userId` (string, required): The distinct ID of the user +- `model` (string, required): The model used (e.g., `gpt-4`, `claude-3`) +- `provider` (string, required): The LLM provider (e.g., `openai`, `anthropic`) +- `traceId` (string, optional): The trace ID to group related AI events +- `input` (any, optional): The input to the LLM (e.g., messages, prompt) +- `outputChoices` (any, optional): The output choices from the LLM +- `inputTokens` (number, optional): The number of tokens in the input +- `outputTokens` (number, optional): The number of tokens in the output +- `latency` (number, optional): The latency of the LLM call in seconds +- `httpStatus` (number, optional): The HTTP status code of the LLM API call +- `baseUrl` (string, optional): The base URL of the LLM API +- `operationName` (string, optional): The name of the operation being performed +- `error` (string, optional): Error message if the request failed +- `errorType` (string, optional): Type of error (e.g., rate_limit, timeout) +- `mcpToolsUsed` (string[], optional): List of MCP tools used during the request + +### Parameters Comparison + +| Parameter | Type | Required | Description | PostHog | OpenTelemetry | +| --------------- | ------------------- | -------- | ----------------------------------------------- | ------- | ------------- | +| `userId` | `string` | Yes | The distinct ID of the user. | ✅ | ✅ | +| `model` | `string` | Yes | The model used (e.g., `gpt-4`, `claude-3`). | ✅ | ✅ | +| `provider` | `string` | Yes | The LLM provider (e.g., `openai`, `anthropic`). | ✅ | ✅ | +| `traceId` | `string` | No | The trace ID to group related AI events. | ✅ | ✅ | +| `input` | `any` | No | The input to the LLM (e.g., messages, prompt). | ✅ | ✅ | +| `outputChoices` | `any` | No | The output choices from the LLM. | ✅ | ✅ | +| `inputTokens` | `number` | No | The number of tokens in the input. | ✅ | ✅ | +| `outputTokens` | `number` | No | The number of tokens in the output. | ✅ | ✅ | +| `latency` | `number` | No | The latency of the LLM call in seconds. | ✅ | ✅ | +| `httpStatus` | `number` | No | The HTTP status code of the LLM API call. | ✅ | ✅ | +| `baseUrl` | `string` | No | The base URL of the LLM API. | ✅ | ✅ | +| `operationName` | `string` | No | The name of the operation being performed. | ❌ | ✅ | +| `error` | `string` | No | Error message if the request failed. | ❌ | ✅ | +| `errorType` | `string` | No | Type of error (e.g., rate_limit, timeout). | ❌ | ✅ | +| `mcpToolsUsed` | `string[]` | No | List of MCP tools used during the request. | ❌ | ✅ | ## Development @@ -203,6 +320,11 @@ This is the core tool provided by the server. It captures LLM usage in PostHog f - **Run tests**: `npm test` - **Lint and format**: `npm run lint` and `npm run format` +## Documentation + +- [OpenTelemetry Documentation](docs/opentelemetry.md) - Complete OpenTelemetry configuration, usage, and examples. +- [Environment Configuration](.env.example) - All available configuration options. + ## License [MIT License](https://opensource.org/licenses/MIT) diff --git a/docs/opentelemetry.md b/docs/opentelemetry.md new file mode 100644 index 0000000..02b2ce5 --- /dev/null +++ b/docs/opentelemetry.md @@ -0,0 +1,332 @@ +# OpenTelemetry LLM Observability + +This document provides comprehensive guidance for using the OpenTelemetry LLM observability tool with the MCP server. It covers setup, configuration, usage, troubleshooting, and practical examples for all major OpenTelemetry-compatible backends. + +## Features + +- **Universal Compatibility**: Works with Jaeger, New Relic, Grafana, Datadog, Honeycomb, and more +- **Comprehensive Metrics**: Request counts, token usage, latency, error rates +- **Distributed Tracing**: Full request lifecycle tracking with spans +- **Flexible Configuration**: Environment-based configuration for different backends +- **Zero-Code Integration**: Drop-in replacement for existing observability tools + +--- + +## Quick Start + +### 1. Install Dependencies + +OpenTelemetry dependencies are included in `package.json`: + +```bash +npm install +``` + +### 2. Configure Your Backend + +#### Jaeger (Local Development) + +```bash +docker run -d --name jaeger \ + -e COLLECTOR_OTLP_ENABLED=true \ + -p 16686:16686 \ + -p 4317:4317 \ + -p 4318:4318 \ + jaegertracing/all-in-one:latest +export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +export OTEL_SERVICE_NAME=llm-observability-mcp +``` + +#### New Relic + +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4318 +export OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY" +export OTEL_SERVICE_NAME=llm-observability-mcp +``` + +#### Grafana Cloud + +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp +export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $(echo -n YOUR_INSTANCE_ID:YOUR_API_KEY | base64)" +export OTEL_SERVICE_NAME=llm-observability-mcp +``` + +#### Honeycomb + +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io/v1/traces +export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=YOUR_API_KEY" +export OTEL_SERVICE_NAME=llm-observability-mcp +``` + +#### Datadog + +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com/api/v2/series +export OTEL_EXPORTER_OTLP_HEADERS="DD-API-KEY=YOUR_DD_API_KEY" +export OTEL_SERVICE_NAME=llm-observability-mcp +``` + +#### Lightstep + +```bash +export OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.lightstep.com:443/api/v2/otel/trace +export OTEL_EXPORTER_OTLP_HEADERS="lightstep-access-token=YOUR_ACCESS_TOKEN" +export OTEL_SERVICE_NAME=llm-observability-mcp +``` + +#### Kubernetes Example + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: llm-observability-mcp +spec: + replicas: 3 + selector: + matchLabels: + app: llm-observability-mcp + template: + metadata: + labels: + app: llm-observability-mcp + spec: + containers: + - name: llm-observability-mcp + image: llm-observability-mcp:latest + ports: + - containerPort: 3000 + env: + - name: OTEL_SERVICE_NAME + value: "llm-observability-mcp" + - name: OTEL_SERVICE_VERSION + value: "1.2.3" + - name: OTEL_ENVIRONMENT + value: "production" + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: "https://your-backend.com:4318" + - name: OTEL_EXPORTER_OTLP_HEADERS + valueFrom: + secretKeyRef: + name: otel-credentials + key: headers +``` + +--- + +## Running the MCP Server + +```bash +# Start with stdio transport +npm run mcp:stdio +# Start with HTTP transport +npm run mcp:http +``` + +--- + +## Usage + +### OpenTelemetry Tool: `llm_observability_otel` + +#### Required Parameters + +- `userId`: The distinct ID of the user +- `model`: The model used (e.g., "gpt-4", "claude-3") +- `provider`: The LLM provider (e.g., "openai", "anthropic") + +#### Optional Parameters + +- `traceId`: Trace ID for grouping related events +- `input`: The input to the LLM (messages, prompt, etc.) +- `outputChoices`: The output from the LLM +- `inputTokens`: Number of tokens in the input +- `outputTokens`: Number of tokens in the output +- `latency`: Latency of the LLM call in seconds +- `httpStatus`: HTTP status code of the LLM call +- `baseUrl`: Base URL of the LLM API +- `operationName`: Name of the operation being performed +- `error`: Error message if the request failed +- `errorType`: Type of error (e.g., "rate_limit", "timeout") +- `mcpToolsUsed`: List of MCP tools used during the request + +#### Example Usage + +```json +{ + "tool": "llm_observability_otel", + "arguments": { + "userId": "user-12345", + "model": "gpt-4", + "provider": "openai", + "inputTokens": 150, + "outputTokens": 75, + "latency": 2.3, + "httpStatus": 200, + "operationName": "chat-completion", + "traceId": "trace-abc123", + "input": "What is the weather like today?", + "outputChoices": ["The weather is sunny and 75°F today."] + } +} +``` + +--- + +## Configuration Reference + +| Variable | Description | Default | +|----------|-------------|---------| +| `OTEL_SERVICE_NAME` | Service name for OpenTelemetry | `llm-observability-mcp` | +| `OTEL_SERVICE_VERSION` | Service version | `1.0.0` | +| `OTEL_ENVIRONMENT` | Environment name | `development` | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | Default OTLP endpoint | - | +| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Metrics endpoint | - | +| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Traces endpoint | - | +| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Logs endpoint | - | +| `OTEL_EXPORTER_OTLP_HEADERS` | Headers for authentication (format: "key1=value1,key2=value2") | - | +| `OTEL_METRIC_EXPORT_INTERVAL` | Metrics export interval in ms | `10000` | +| `OTEL_METRIC_EXPORT_TIMEOUT` | Metrics export timeout in ms | `5000` | +| `OTEL_TRACES_SAMPLER_ARG` | Sampling ratio (0.0-1.0) | `1.0` | + +--- + +## Metrics Collected + +- `llm.requests.total`: Total number of LLM requests +- `llm.tokens.total`: Total tokens used (input + output) +- `llm.latency.duration`: Request latency in milliseconds +- `llm.requests.active`: Number of active requests + +### Trace Attributes + +- `llm.model`, `llm.provider`, `llm.user_id`, `llm.operation`, `llm.input_tokens`, `llm.output_tokens`, `llm.total_tokens`, `llm.latency_ms`, `llm.http_status`, `llm.base_url`, `llm.error`, `llm.error_type`, `llm.input`, `llm.output`, `llm.mcp_tools_used` + +--- + +## Practical Examples + +### Jaeger: View Traces + +Open to see your traces. + +### Error Tracking Example + +```json +{ + "tool": "llm_observability_otel", + "arguments": { + "userId": "user-12345", + "model": "gpt-4", + "provider": "openai", + "httpStatus": 429, + "error": "Rate limit exceeded", + "errorType": "rate_limit", + "latency": 0.1, + "operationName": "chat-completion" + } +} +``` + +### Multi-Tool Usage Tracking Example + +```json +{ + "tool": "llm_observability_otel", + "arguments": { + "userId": "user-12345", + "model": "gpt-4", + "provider": "openai", + "inputTokens": 500, + "outputTokens": 200, + "latency": 5.2, + "httpStatus": 200, + "operationName": "complex-workflow", + "mcpToolsUsed": ["file_read", "web_search", "code_execution"], + "traceId": "complex-workflow-123" + } +} +``` + +### Testing Script + +```bash +#!/bin/bash +# test-opentelemetry.sh + +docker run -d --name jaeger-test \ + -e COLLECTOR_OTLP_ENABLED=true \ + -p 16686:16686 \ + -p 4318:4318 \ + jaegertracing/all-in-one:latest +sleep 5 +export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +export OTEL_SERVICE_NAME=llm-observability-test +export OTEL_ENVIRONMENT=test +npm run mcp:stdio & +sleep 3 +curl -X POST http://localhost:3000/mcp \ + -H "Content-Type: application/json" \ + -d '{ + "tool": "llm_observability_otel", + "arguments": { + "userId": "test-user", + "model": "gpt-4", + "provider": "openai", + "inputTokens": 100, + "outputTokens": 50, + "latency": 1.5, + "httpStatus": 200, + "operationName": "test-completion" + } + }' +echo "Test complete. View traces at http://localhost:16686" +``` + +--- + +## Migration from PostHog + +The OpenTelemetry tool is a drop-in replacement for the PostHog tool. Both can coexist for gradual migration: + +- **PostHog Tool**: `llm_observability_posthog` +- **OpenTelemetry Tool**: `llm_observability_otel` + +Both accept the same parameters. + +--- + +## Troubleshooting & Performance + +### Common Issues + +- No data in backend: check endpoint URLs, authentication, network, server logs +- High resource usage: lower sampling (`OTEL_TRACES_SAMPLER_ARG`), increase export intervals +- Missing traces: verify OpenTelemetry is enabled, check logs, service name + +### Debug Mode + +```bash +export DEBUG=true +npm run mcp:stdio +``` + +### Performance Tuning + +- Reduce sampling for high-volume: `OTEL_TRACES_SAMPLER_ARG=0.01` +- Increase export intervals: `OTEL_METRIC_EXPORT_INTERVAL=60000` +- Disable metrics/logs if not needed: `unset OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, `unset OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` + +--- + +## Support + +For issues or questions: + +1. Check this document and troubleshooting +2. Review server logs with `DEBUG=true` +3. Verify OpenTelemetry configuration +4. Test with Jaeger locally first diff --git a/eslint.config.mjs b/eslint.config.mjs index 554c375..16d3c48 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -42,5 +42,16 @@ export default tseslint.config( '@typescript-eslint/no-unused-vars': 'off', }, }, + // Rules for scripts + { + files: ['scripts/**/*.js'], // Target all JS files in the scripts directory + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + env: { + node: true, // Enable Node.js global variables and Node.js scoping + }, + }, eslintConfigPrettier, ); diff --git a/package-lock.json b/package-lock.json index e39ce2b..ad1590e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,15 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.15.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-otlp-grpc": "^0.26.0", + "@opentelemetry/exporter-otlp-http": "^0.26.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/resources": "^2.0.1", + "@opentelemetry/sdk-metrics": "^2.0.1", + "@opentelemetry/sdk-node": "^0.203.0", + "@opentelemetry/sdk-trace-node": "^2.0.1", + "@opentelemetry/semantic-conventions": "^1.36.0", "commander": "^14.0.0", "cors": "^2.8.5", "dotenv": "^17.2.0", @@ -811,6 +820,182 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", + "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/grpc-js/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js/node_modules/protobufjs": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", + "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", + "license": "Apache-2.0", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.11.3", + "yargs": "^16.2.0" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@grpc/proto-loader/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/@grpc/proto-loader/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1443,6 +1628,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.1.tgz", @@ -1673,6 +1868,1165 @@ "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-metrics": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-metrics/-/api-metrics-0.26.0.tgz", + "integrity": "sha512-idDSUTx+LRwJiHhVHhdh45SWow5u9lKNDROKu5AMzsIVPI29utH5FfT9vor8qMM6blxWWvlT22HUNdNMWqUQfQ==", + "deprecated": "Please use @opentelemetry/api >= 1.3.0", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.2" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.0.1.tgz", + "integrity": "sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.0.0.tgz", + "integrity": "sha512-1+qvKilADnSFW4PiXy+f7D22pvfGVxepZ69GcbF8cTcbQTUt7w63xEBWn5f5j92x9I3c0sqbW1RUx5/a4wgzxA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">=8.5.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.2" + } + }, + "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.0.0.tgz", + "integrity": "sha512-XCZ6ZSmc8FOspxKUU+Ow9UtJeSSRcS5rFBYGpjzix02U2v+X9ofjOjgNRnpvxlSvkccYIhdTuwcvNskmZ46SeA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.203.0.tgz", + "integrity": "sha512-g/2Y2noc/l96zmM+g0LdeuyYKINyBwN6FJySoU15LHPLcMN/1a0wNk2SegwKcxrRdE7Xsm7fkIR5n6XFe3QpPw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", + "@opentelemetry/sdk-logs": "0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.203.0.tgz", + "integrity": "sha512-s0hys1ljqlMTbXx2XiplmMJg9wG570Z5lH7wMvrZX6lcODI56sG4HL03jklF63tBeyNwK2RV1/ntXGo3HgG4Qw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", + "@opentelemetry/sdk-logs": "0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.203.0.tgz", + "integrity": "sha512-nl/7S91MXn5R1aIzoWtMKGvqxgJgepB/sH9qW0rZvZtabnsjbf8OQ1uSx3yogtvLr0GzwD596nQKz2fV7q2RBw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "@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-logs": "0.203.0", + "@opentelemetry/sdk-trace-base": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.203.0.tgz", + "integrity": "sha512-FCCj9nVZpumPQSEI57jRAA89hQQgONuoC35Lt+rayWY/mzCAc6BQT7RFyFaZKJ2B7IQ8kYjOCPsF/HGFWjdQkQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.203.0", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-grpc-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/core": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", + "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "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-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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.203.0.tgz", + "integrity": "sha512-OZnhyd9npU7QbyuHXFEPVm3LnjZYifuKpT3kTnF84mXeEQ84pJJZgyLBpU4FSkSwUkt/zbMyNAI7y5+jYTWGIg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/exporter-metrics-otlp-http": "0.203.0", + "@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/core": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", + "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-otlp-grpc": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-otlp-grpc/-/exporter-otlp-grpc-0.26.0.tgz", + "integrity": "sha512-64VPck7CbGhI7c2bj54xaGGHCy4mP+rMnrKBAruNBmUUnN9OgihddNcMsIiHyddUcyC1I+hXS3JLW1G6AvlAmg==", + "deprecated": "Please use trace and metric specific exporters @opentelemetry/exporter-trace-otlp-grpc and @opentelemetry/exporter-metrics-otlp-grpc", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.3.7", + "@grpc/proto-loader": "^0.6.4", + "@opentelemetry/core": "1.0.0", + "@opentelemetry/exporter-otlp-http": "0.26.0", + "@opentelemetry/resources": "1.0.0", + "@opentelemetry/sdk-metrics-base": "0.26.0", + "@opentelemetry/sdk-trace-base": "1.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.2" + } + }, + "node_modules/@opentelemetry/exporter-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.0.0.tgz", + "integrity": "sha512-ORP8F2LLcJEm5M3H24RmdlMdiDc70ySPushpkrAW34KZGdZXwkrFoFXZhhs5MUxPT+fLrTuBafXxZVr8eHtFuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.0.0", + "@opentelemetry/semantic-conventions": "1.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.2" + } + }, + "node_modules/@opentelemetry/exporter-otlp-grpc/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.0.0.tgz", + "integrity": "sha512-XCZ6ZSmc8FOspxKUU+Ow9UtJeSSRcS5rFBYGpjzix02U2v+X9ofjOjgNRnpvxlSvkccYIhdTuwcvNskmZ46SeA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/exporter-otlp-http": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-otlp-http/-/exporter-otlp-http-0.26.0.tgz", + "integrity": "sha512-V3FcUEIVDZ66b3/6vjSBjwwozf/XV5eUXuELNzN8PAvGZH4mw36vaWlaxnGEV8HaZb2hbu2KbRpcOzqxx3tFDA==", + "deprecated": "Please use trace and metric specific exporters @opentelemetry/exporter-trace-otlp-http and @opentelemetry/exporter-metrics-otlp-http", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-metrics": "0.26.0", + "@opentelemetry/core": "1.0.0", + "@opentelemetry/resources": "1.0.0", + "@opentelemetry/sdk-metrics-base": "0.26.0", + "@opentelemetry/sdk-trace-base": "1.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.2" + } + }, + "node_modules/@opentelemetry/exporter-otlp-http/node_modules/@opentelemetry/resources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.0.0.tgz", + "integrity": "sha512-ORP8F2LLcJEm5M3H24RmdlMdiDc70ySPushpkrAW34KZGdZXwkrFoFXZhhs5MUxPT+fLrTuBafXxZVr8eHtFuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.0.0", + "@opentelemetry/semantic-conventions": "1.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.2" + } + }, + "node_modules/@opentelemetry/exporter-otlp-http/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.0.0.tgz", + "integrity": "sha512-XCZ6ZSmc8FOspxKUU+Ow9UtJeSSRcS5rFBYGpjzix02U2v+X9ofjOjgNRnpvxlSvkccYIhdTuwcvNskmZ46SeA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.203.0.tgz", + "integrity": "sha512-2jLuNuw5m4sUj/SncDf/mFPabUxMZmmYetx5RKIMIQyPnl6G6ooFzfeE8aXNRf8YD1ZXNlCnRPcISxjveGJHNg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@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-prometheus/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.203.0.tgz", + "integrity": "sha512-322coOTf81bm6cAA8+ML6A+m4r2xTCdmAZzGNTboPXRzhwPt4JEmovsFAs+grpdarObd68msOJ9FfH3jxM6wqA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.203.0.tgz", + "integrity": "sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw==", + "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-trace-base": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.203.0.tgz", + "integrity": "sha512-1xwNTJ86L0aJmWRwENCJlH4LULMG2sOXWIVw+Szta4fkqKVY50Eo4HoVKKq6U9QEytrWCr8+zjw0q/ZOeXpcAQ==", + "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-trace-base": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.0.1.tgz", + "integrity": "sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.203.0.tgz", + "integrity": "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-transformer": "0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.203.0.tgz", + "integrity": "sha512-te0Ze1ueJF+N/UOFl5jElJW4U0pZXQ8QklgSfJ2linHN0JJsuaHG8IabEUi2iqxY8ZBDlSiz1Trfv5JcjWWWwQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/otlp-exporter-base": "0.203.0", + "@opentelemetry/otlp-transformer": "0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "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/otlp-transformer/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/protobufjs": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", + "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.0.1.tgz", + "integrity": "sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-b3/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.0.1.tgz", + "integrity": "sha512-7PMdPBmGVH2eQNb/AtSJizQNgeNTfh6jQFqys6lfhd6P4r+m/nTh3gKPPpaCXVdRQ+z93vfKk+4UGty390283w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.203.0.tgz", + "integrity": "sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", + "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics-base": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics-base/-/sdk-metrics-base-0.26.0.tgz", + "integrity": "sha512-PbJsso7Vy/CLATAOyXbt/VP7ZQ2QYnvlq28lhOWaLPw8aqLogMBvidNGRrt7rF4/hfzLT6pMgpAAcit2C/nUMA==", + "deprecated": "Please use @opentelemetry/sdk-metrics", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-metrics": "0.26.0", + "@opentelemetry/core": "1.0.0", + "@opentelemetry/resources": "1.0.0", + "lodash.merge": "^4.6.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.2" + } + }, + "node_modules/@opentelemetry/sdk-metrics-base/node_modules/@opentelemetry/resources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.0.0.tgz", + "integrity": "sha512-ORP8F2LLcJEm5M3H24RmdlMdiDc70ySPushpkrAW34KZGdZXwkrFoFXZhhs5MUxPT+fLrTuBafXxZVr8eHtFuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.0.0", + "@opentelemetry/semantic-conventions": "1.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.2" + } + }, + "node_modules/@opentelemetry/sdk-metrics-base/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.0.0.tgz", + "integrity": "sha512-XCZ6ZSmc8FOspxKUU+Ow9UtJeSSRcS5rFBYGpjzix02U2v+X9ofjOjgNRnpvxlSvkccYIhdTuwcvNskmZ46SeA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.203.0.tgz", + "integrity": "sha512-zRMvrZGhGVMvAbbjiNQW3eKzW/073dlrSiAKPVWmkoQzah9wfynpVPeL55f9fVIm0GaBxTLcPeukWGy0/Wj7KQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/exporter-logs-otlp-grpc": "0.203.0", + "@opentelemetry/exporter-logs-otlp-http": "0.203.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.203.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.203.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.203.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.203.0", + "@opentelemetry/exporter-prometheus": "0.203.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.203.0", + "@opentelemetry/exporter-trace-otlp-http": "0.203.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.203.0", + "@opentelemetry/exporter-zipkin": "2.0.1", + "@opentelemetry/instrumentation": "0.203.0", + "@opentelemetry/propagator-b3": "2.0.1", + "@opentelemetry/propagator-jaeger": "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/sdk-trace-node": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.0.0.tgz", + "integrity": "sha512-/rXoyQlDlJTJ4SOVAbP0Gpj89B8oZ2hJApYG2Dq5klkgFAtDifN8271TIzwtM8/ET8HUhgx9eyoUJi42LhIesg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.0.0", + "@opentelemetry/resources": "1.0.0", + "@opentelemetry/semantic-conventions": "1.0.0", + "lodash.merge": "^4.6.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.2" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.0.0.tgz", + "integrity": "sha512-ORP8F2LLcJEm5M3H24RmdlMdiDc70ySPushpkrAW34KZGdZXwkrFoFXZhhs5MUxPT+fLrTuBafXxZVr8eHtFuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.0.0", + "@opentelemetry/semantic-conventions": "1.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.2" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.0.0.tgz", + "integrity": "sha512-XCZ6ZSmc8FOspxKUU+Ow9UtJeSSRcS5rFBYGpjzix02U2v+X9ofjOjgNRnpvxlSvkccYIhdTuwcvNskmZ46SeA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.0.1.tgz", + "integrity": "sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.0.1", + "@opentelemetry/core": "2.0.1", + "@opentelemetry/sdk-trace-base": "2.0.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/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==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/resources": "2.0.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.36.0.tgz", + "integrity": "sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1742,6 +3096,70 @@ "node": ">=12" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -2404,6 +3822,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -2415,7 +3839,6 @@ "version": "24.0.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.8.0" @@ -3023,7 +4446,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3032,6 +4454,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -3115,7 +4546,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3741,7 +5171,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -3756,7 +5185,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3766,14 +5194,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -3788,7 +5214,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -3801,7 +5226,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -3837,7 +5261,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3850,7 +5273,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -4541,7 +5963,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5346,7 +6767,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -5773,6 +7193,24 @@ "node": ">=18.20" } }, + "node_modules/import-in-the-middle": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.14.2.tgz", + "integrity": "sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/import-in-the-middle/node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -5895,6 +7333,21 @@ "dev": true, "license": "MIT" }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5909,7 +7362,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7129,6 +8581,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.capitalize": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", @@ -7168,7 +8626,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, "license": "MIT" }, "node_modules/lodash.uniqby": { @@ -7178,6 +8635,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7442,6 +8905,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10593,6 +12062,12 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -10947,6 +12422,32 @@ "dev": true, "license": "ISC" }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -11196,12 +12697,45 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -11485,7 +13019,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -12169,6 +13702,18 @@ "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/synckit": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", @@ -12585,7 +14130,6 @@ "version": "7.8.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true, "license": "MIT" }, "node_modules/unicode-emoji-modifier-base": { @@ -12964,7 +14508,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -12981,7 +14524,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -13000,7 +14542,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -13010,7 +14551,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13020,14 +14560,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -13042,7 +14580,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" diff --git a/package.json b/package.json index 4d147f7..1be81dd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@sfiorini/llm-observability-mcp", "version": "0.1.0", - "description": "A Model Context Protocol (MCP) server that provides a tool to capture LLM Observability events and send them to PostHog.", + "description": "A Model Context Protocol (MCP) server that provides comprehensive LLM observability tools supporting both PostHog and OpenTelemetry backends.", "main": "dist/index.js", "types": "dist/index.d.ts", "type": "commonjs", @@ -20,9 +20,7 @@ "test": "jest", "test:coverage": "jest --coverage", "lint": "eslint src --ext .ts --config eslint.config.mjs", - "update:deps": "npx npm-check-updates -u && npm install --legacy-peer-deps", "format": "prettier --write 'src/**/*.ts' 'scripts/**/*.js'", - "cli": "npm run build && node dist/index.js", "mcp:stdio": "TRANSPORT_MODE=stdio npm run build && node dist/index.js", "mcp:http": "TRANSPORT_MODE=http npm run build && node dist/index.js", "mcp:inspect": "TRANSPORT_MODE=http npm run build && (node dist/index.js &) && sleep 2 && npx @modelcontextprotocol/inspector http://localhost:3000/mcp", @@ -76,6 +74,15 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.15.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-otlp-grpc": "^0.26.0", + "@opentelemetry/exporter-otlp-http": "^0.26.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/resources": "^2.0.1", + "@opentelemetry/sdk-metrics": "^2.0.1", + "@opentelemetry/sdk-node": "^0.203.0", + "@opentelemetry/sdk-trace-node": "^2.0.1", + "@opentelemetry/semantic-conventions": "^1.36.0", "commander": "^14.0.0", "cors": "^2.8.5", "dotenv": "^17.2.0", @@ -113,4 +120,4 @@ ".ts" ] } -} \ No newline at end of file +} diff --git a/src/cli/index.ts b/src/cli/index.ts deleted file mode 100644 index 334e34b..0000000 --- a/src/cli/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Command } from 'commander'; -import { Logger } from '../utils/logger.util.js'; -import { VERSION, CLI_NAME } from '../utils/constants.util.js'; - -import posthogLlmCli from './posthog-llm.cli.js'; - -/** - * CLI entry point for the LLM Log MCP Server - * - * This file registers all CLI commands and handles command line parsing - */ - -// Package description -const DESCRIPTION = - 'A LLM Log Model Context Protocol (MCP) server implementation using TypeScript'; - -/** - * Run the CLI with the provided arguments - * - * @param args Command line arguments to process - * @returns Promise that resolves when CLI command execution completes - */ -export async function runCli(args: string[]) { - const cliLogger = Logger.forContext('cli/index.ts', 'runCli'); - cliLogger.debug('Initializing CLI with arguments', args); - - const program = new Command(); - - program.name(CLI_NAME).description(DESCRIPTION).version(VERSION); - - // Register CLI commands - cliLogger.debug('Registering CLI commands...'); - posthogLlmCli.register(program); - cliLogger.debug('CLI commands registered successfully'); - - // Handle unknown commands - program.on('command:*', (operands) => { - cliLogger.error(`Unknown command: ${operands[0]}`); - console.log(''); - program.help(); - process.exit(1); - }); - - // Parse arguments; default to help if no command provided - cliLogger.debug('Parsing CLI arguments'); - - // Special handling for top-level commands - if (args.length === 1) { - // Check if it's a known top-level command - const command = program.commands.find((cmd) => cmd.name() === args[0]); - if (command) { - command.outputHelp(); - process.exit(0); - } - } - await program.parseAsync(args.length ? args : ['--help'], { from: 'user' }); - cliLogger.debug('CLI command execution completed'); -} diff --git a/src/cli/posthog-llm.cli.ts b/src/cli/posthog-llm.cli.ts deleted file mode 100644 index 2140bba..0000000 --- a/src/cli/posthog-llm.cli.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Command } from 'commander'; -import { Logger } from '../utils/logger.util.js'; -import { handleCliError } from '../utils/error.util.js'; -import postHogLlmController from '../controllers/posthog-llm.controller.js'; - -const logger = Logger.forContext('cli/posthog-llm.cli.ts'); - -/** - * Register PostHog LLM CLI commands - * @param program The Commander program instance - */ -function register(program: Command) { - const methodLogger = logger.forMethod('register'); - methodLogger.debug('Registering PostHog LLM CLI commands...'); - - program - .command('llm-observability-mcp') - .description('Interact with the PostHog LLM API.') - .argument('', 'The name of the event to capture.') - .argument('', 'The distinct ID of the user.') - .option( - '-p, --properties ', - 'JSON string of event properties. Can be a single quoted string or raw key-value pairs.', - ) - .action(async (eventName, distinctId, options) => { - const actionLogger = logger.forMethod('action:capture'); - try { - actionLogger.debug(`CLI posthog-llm capture called`, { - eventName, - distinctId, - options, - }); - - // Handle JSON properties from command object - let properties = {}; - if (options.properties && options.properties.length > 0) { - const propertiesString = options.properties.join(' '); - try { - properties = JSON.parse(propertiesString); - } catch { - // Try to fix common issues with JSON formatting - const fixedJson = propertiesString - .replace(/(\w+):/g, '"$1":') // Add quotes around keys - .replace(/'/g, '"'); // Replace single quotes with double quotes - - properties = JSON.parse(fixedJson); - } - } - - const args = { eventName, distinctId, properties }; - await postHogLlmController.capture(args); - } catch (error) { - handleCliError(error); - } - }); - - methodLogger.debug('PostHog LLM CLI commands registered successfully'); -} - -export default { register }; diff --git a/src/config/common.schema.ts b/src/config/common.schema.ts new file mode 100644 index 0000000..e1b3885 --- /dev/null +++ b/src/config/common.schema.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; + +/** + * Common/shared configuration options schema (now includes global options) + */ +export const CommonConfigSchema = z.object({ + serviceName: z.string().default('llm-observability-mcp'), + serviceVersion: z.string().default('1.0.0'), + environment: z.string().default('development'), + debug: z + .preprocess((val) => { + if (typeof val === 'string') { + if (val.toLowerCase() === 'false') return false; + if (val.toLowerCase() === 'true') return true; + } + return val; + }, z.boolean()) + .default(false), + logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'), +}); + +export type CommonConfigType = z.infer; diff --git a/src/config/config-loader.ts b/src/config/config-loader.ts new file mode 100644 index 0000000..6c9f049 --- /dev/null +++ b/src/config/config-loader.ts @@ -0,0 +1,202 @@ +import fs from 'fs'; +import path from 'path'; +import { Logger } from '../utils/logger.util.js'; +import dotenv from 'dotenv'; +import os from 'os'; +import { CommonConfigSchema, CommonConfigType } from './common.schema.js'; +import { PostHogConfigSchema, PostHogConfigType } from './posthog.schema.js'; +import { + OpenTelemetryConfigSchema, + OpenTelemetryConfigType, +} from './opentelemetry-llm.schema.js'; +import { z } from 'zod'; + +/** + * Configuration loader that handles multiple sources with priority: + * 1. Direct ENV pass (process.env) + * 2. .env file in project root + * 3. Global config file at $HOME/.mcp/configs.json + */ +class ConfigLoader { + private packageName: string; + private configLoaded: boolean = false; + + constructor(packageName: string) { + this.packageName = packageName; + } + + load(): void { + const logger = Logger.forContext('config-loader', 'load'); + + if (this.configLoaded) { + logger.debug('Configuration already loaded, skipping'); + return; + } + + logger.debug('Loading configuration...'); + + // Load from global config file + this.loadFromGlobalConfig(); + + // Load from .env file + this.loadFromEnvFile(); + + this.configLoaded = true; + logger.debug('Configuration loaded successfully'); + } + + private loadFromEnvFile(): void { + const logger = Logger.forContext('config-loader', 'loadFromEnvFile'); + + try { + const result = dotenv.config(); + if (result.error) { + logger.debug('No .env file found or error reading it'); + return; + } + logger.debug('Loaded configuration from .env file'); + } catch (error) { + logger.error('Error loading .env file', error); + } + } + + private loadFromGlobalConfig(): void { + const logger = Logger.forContext( + 'config-loader', + 'loadFromGlobalConfig', + ); + + try { + const homedir = os.homedir(); + const globalConfigPath = path.join(homedir, '.mcp', 'configs.json'); + + if (!fs.existsSync(globalConfigPath)) { + logger.debug('Global config file not found'); + return; + } + + const configContent = fs.readFileSync(globalConfigPath, 'utf8'); + const config = JSON.parse(configContent); + + const shortKey = 'llm-observability-mcp'; + const fullPackageName = this.packageName; + const unscopedPackageName = + fullPackageName.split('/')[1] || fullPackageName; + + const potentialKeys = [ + shortKey, + fullPackageName, + unscopedPackageName, + ]; + let foundConfig = null; + + for (const key of potentialKeys) { + if ( + config[key] && + typeof config[key] === 'object' && + config[key].environments + ) { + foundConfig = config[key]; + logger.debug(`Found configuration using key: ${key}`); + break; + } + } + + if (!foundConfig || !foundConfig.environments) { + logger.debug( + `No config found for ${this.packageName} using keys: ${potentialKeys.join(', ')}`, + ); + return; + } + + for (const [key, value] of Object.entries( + foundConfig.environments, + )) { + if (process.env[key] === undefined) { + process.env[key] = String(value); + } + } + + logger.debug(`Loaded configuration from global config file`); + } catch (error) { + logger.error('Error loading global config file', error); + } + } + + private getEnvObject(schema: z.ZodTypeAny): Record { + const shape = + typeof schema._def.shape === 'function' + ? schema._def.shape() + : schema._def.shape; + const result: Record = {}; + for (const key in shape) { + if (Object.prototype.hasOwnProperty.call(shape, key)) { + const envKey = this.toEnvKey(key); + const value = process.env[envKey]; + if (value !== undefined) { + const type = shape[key]._def.typeName; + if (type === 'ZodNumber') { + result[key] = Number(value); + } else if (type === 'ZodBoolean') { + result[key] = value.toLowerCase() === 'true'; + } else { + result[key] = value; + } + } + } + } + return result; + } + + private toEnvKey(key: string): string { + // Map schema keys to environment variable names + // e.g. serviceName -> OTEL_SERVICE_NAME, apiKey -> POSTHOG_API_KEY, etc. + const map: Record = { + serviceName: 'OTEL_SERVICE_NAME', + serviceVersion: 'OTEL_SERVICE_VERSION', + environment: 'OTEL_ENVIRONMENT', + metricsEndpoint: 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT', + tracesEndpoint: 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', + logsEndpoint: 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', + headers: 'OTEL_EXPORTER_OTLP_HEADERS', + exportIntervalMillis: 'OTEL_METRIC_EXPORT_INTERVAL', + exportTimeoutMillis: 'OTEL_METRIC_EXPORT_TIMEOUT', + samplingRatio: 'OTEL_TRACES_SAMPLER_ARG', + apiKey: 'POSTHOG_API_KEY', + host: 'POSTHOG_HOST', + debug: 'DEBUG', + logLevel: 'LOG_LEVEL', + }; + return map[key] || key; + } + + getCommonConfig(): CommonConfigType { + return CommonConfigSchema.parse(this.getEnvObject(CommonConfigSchema)); + } + + getPosthogConfig(): PostHogConfigType { + return PostHogConfigSchema.parse( + this.getEnvObject(PostHogConfigSchema), + ); + } + + getOpenTelemetryConfig(): OpenTelemetryConfigType { + const raw = this.getEnvObject(OpenTelemetryConfigSchema); + // Special handling for headers (parse comma-separated string to object) + if (typeof raw.headers === 'string') { + const headers: Record = {}; + const pairs = raw.headers.split(','); + for (const pair of pairs) { + const [key, value] = pair.split('='); + if (key && value) { + headers[key.trim()] = value.trim(); + } + } + raw.headers = headers; + } + return OpenTelemetryConfigSchema.parse(raw); + } +} + +const configLoader = new ConfigLoader('@sfiorini/llm-observability-mcp'); +export default configLoader; diff --git a/src/config/opentelemetry-llm.schema.ts b/src/config/opentelemetry-llm.schema.ts new file mode 100644 index 0000000..978b6ae --- /dev/null +++ b/src/config/opentelemetry-llm.schema.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; +import { CommonConfigSchema } from './common.schema.js'; + +/** + * Configuration schema for OpenTelemetry + */ +export const OpenTelemetryConfigSchema = CommonConfigSchema.extend({ + // OTLP endpoints + metricsEndpoint: z.string().optional(), + tracesEndpoint: z.string().optional(), + logsEndpoint: z.string().optional(), + + // Authentication headers + headers: z.record(z.string()).optional(), + + // Export configuration + exportIntervalMillis: z.number().default(10000), + exportTimeoutMillis: z.number().default(5000), + + // Sampling configuration + samplingRatio: z.number().min(0).max(1).default(1.0), +}); + +export type OpenTelemetryConfigType = z.infer; diff --git a/src/config/posthog.schema.ts b/src/config/posthog.schema.ts new file mode 100644 index 0000000..615c73b --- /dev/null +++ b/src/config/posthog.schema.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; +import { CommonConfigSchema } from './common.schema.js'; + +/** + * PostHog configuration options schema + */ +export const PostHogConfigSchema = CommonConfigSchema.extend({ + apiKey: z.string().describe('PostHog API key'), + host: z + .string() + .default('https://app.posthog.com') + .describe('PostHog host URL'), +}); + +export type PostHogConfigType = z.infer; diff --git a/src/controllers/base.controller.ts b/src/controllers/base.controller.ts new file mode 100644 index 0000000..969598c --- /dev/null +++ b/src/controllers/base.controller.ts @@ -0,0 +1,8 @@ +import { ControllerResponse } from '../types/common.types.js'; + +export abstract class BaseController { + // Instance method (must be implemented by subclasses) + static capture(data: unknown): Promise { + throw new Error(`Not implemented: capture: ${data}`); + } +} diff --git a/src/controllers/opentelemetry-llm.controller.ts b/src/controllers/opentelemetry-llm.controller.ts new file mode 100644 index 0000000..6a814cb --- /dev/null +++ b/src/controllers/opentelemetry-llm.controller.ts @@ -0,0 +1,65 @@ +import { Logger } from '../utils/logger.util.js'; +import { BaseController } from './base.controller.js'; +import { OpenTelemetryService } from '../services/opentelemetry-llm.service.js'; +import { OpenTelemetryLlmInputSchemaType } from '../types/opentelemetry-llm.types.js'; +import { ControllerResponse } from '../types/common.types.js'; + +/** + * Controller for OpenTelemetry LLM observability + */ +export class OpenTelemetryController extends BaseController { + /** + * Capture LLM observability data + */ + static async capture( + data: OpenTelemetryLlmInputSchemaType, + ): Promise { + const logger = Logger.forContext( + 'opentelemetry.controller', + 'captureLlmObservability', + ); + logger.debug('Capturing LLM observability data', data); + + try { + // Get and initialize the service + const service = OpenTelemetryService.getInstance(); + service.initialize(); + + // Record the LLM request + service.recordLlmRequest(data); + + const content = `## OpenTelemetry LLM Observability Captured + +Successfully recorded LLM observability data: + +- **Model**: ${data.model} +- **Provider**: ${data.provider} +- **User ID**: ${data.userId} +- **Operation**: ${data.operationName || 'generate'} +- **Input Tokens**: ${data.inputTokens || 'N/A'} +- **Output Tokens**: ${data.outputTokens || 'N/A'} +- **Total Tokens**: ${(data.inputTokens || 0) + (data.outputTokens || 0)} +- **Latency**: ${data.latency ? `${data.latency}s` : 'N/A'} +- **Status**: ${data.error ? 'Error' : 'Success'} +${data.error ? `- **Error**: ${data.error}` : ''} + +### Metrics Recorded +- ✅ Request counter incremented +- ✅ Token usage recorded +- ✅ Latency histogram updated +- ✅ Distributed trace created + +### Trace Details +- **Trace ID**: ${data.traceId || 'auto-generated'} +- **Span Name**: ${data.operationName || 'llm.generate'} +- **Attributes**: model, provider, user_id, tokens, latency, error details + +The data has been sent to your configured OpenTelemetry collector and is now available in your observability platform (Jaeger, New Relic, Grafana, etc.).`; + + return { content }; + } catch (error) { + logger.error('Error capturing LLM observability', error); + throw error; + } + } +} diff --git a/src/controllers/posthog-llm.controller.ts b/src/controllers/posthog-llm.controller.ts index ad19939..0b22c77 100644 --- a/src/controllers/posthog-llm.controller.ts +++ b/src/controllers/posthog-llm.controller.ts @@ -1,32 +1,35 @@ import { Logger } from '../utils/logger.util.js'; import { ControllerResponse } from '../types/common.types.js'; import postHogLlmService from '../services/posthog-llm.service.js'; +import { PostHogLlmInputSchemaType } from '../types/posthog-llm.types.js'; -const logger = Logger.forContext('controllers/posthog-llm.controller.ts'); +/** + * Controller for PostHog LLM observability + */ +export class PostHogController { + /** + * Capture LLM observability data + */ -async function capture(args: { - eventName: string; - distinctId: string; - properties: Record; -}): Promise { - const methodLogger = logger.forMethod('capture'); - methodLogger.debug('Capturing PostHog event...'); - methodLogger.debug('Arguments:', args); + static async capture( + data: PostHogLlmInputSchemaType, + ): Promise { + const logger = Logger.forContext('posthog-llm.controller', 'capture'); + logger.debug('Capturing LLM observability data', data); - try { - await postHogLlmService.capture(args); - return { - content: 'Event captured successfully.', - }; - } catch (error) { - methodLogger.error('Error capturing event:', error); - const errorMessage = - error instanceof Error ? error.message : 'Unknown error'; - methodLogger.error('Error capturing event:', errorMessage); - return { - content: `Failed to capture event: ${errorMessage}`, - }; + try { + await postHogLlmService.capture(data); + return { + content: 'Event captured successfully.', + }; + } catch (error) { + logger.error('Error capturing event:', error); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + logger.error('Error capturing event:', errorMessage); + return { + content: `Failed to capture event: ${errorMessage}`, + }; + } } } - -export default { capture }; diff --git a/src/index.ts b/src/index.ts index 1515eb2..32ebc57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,5 @@ #!/usr/bin/env node import { Logger } from './utils/logger.util.js'; -import { config } from './utils/config.util.js'; -import { runCli } from './cli/index.js'; import { stdioTransport } from './server/stdio.js'; import { streamableHttpTransport } from './server/streamableHttp.js'; @@ -43,18 +41,6 @@ export async function startServer(): Promise { * Main entry point */ async function main() { - const mainLogger = Logger.forContext('index.ts', 'main'); - - // Load configuration - config.load(); - - // CLI mode - if any arguments are provided - if (process.argv.length > 2) { - mainLogger.info('CLI mode detected'); - await runCli(process.argv.slice(2)); - return; - } - // Server mode - determine transport and start server await startServer(); } diff --git a/src/resources/posthog-llm.resource.ts b/src/resources/posthog-llm.resource.ts deleted file mode 100644 index 9925796..0000000 --- a/src/resources/posthog-llm.resource.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Logger } from '../utils/logger.util.js'; -import posthogLlmController from '../controllers/posthog-llm.controller.js'; -import { formatErrorForMcpResource } from '../utils/error.util.js'; - -const logger = Logger.forContext('resources/posthog-llm.resource.ts'); - -/** - * Register PostHog LLM resources with the MCP server - * - * @param server The MCP server instance - */ -function registerResources(server: McpServer) { - const registerLogger = logger.forMethod('registerResources'); - registerLogger.debug('Registering PostHog LLM resources...'); - - // Register the PostHog event capture resource - server.resource( - 'capture_llm_observability', - 'Capture a LLM event in PostHog', - async (uri: URL) => { - const methodLogger = logger.forMethod('posthogEventResource'); - try { - methodLogger.debug('PostHog event resource called', { - uri: uri.toString(), - }); - - // Extract parameters from the URI - // Format: posthog://event_name/distinct_id?properties=JSON - const pathParts = uri.pathname.split('/').filter(Boolean); - const eventName = pathParts[0] || 'event'; - const distinctId = pathParts[1] || 'anonymous'; - - // Parse properties from query parameters if present - let properties: Record = {}; - if (uri.searchParams.has('properties')) { - try { - properties = JSON.parse( - uri.searchParams.get('properties') || '{}', - ); - } catch (e) { - methodLogger.warn('Failed to parse properties JSON', e); - } - } - - // Call the controller to capture the event - const result = await posthogLlmController.capture({ - eventName, - distinctId, - properties, - }); - - // Return the content as a text resource - return { - contents: [ - { - uri: uri.toString(), - text: result.content, - mimeType: 'text/plain', - description: `PostHog event: ${eventName}`, - }, - ], - }; - } catch (error) { - methodLogger.error('Resource error', error); - return formatErrorForMcpResource(error, uri.toString()); - } - }, - ); - - registerLogger.debug('PostHog LLM resources registered successfully'); -} - -export default { registerResources }; diff --git a/src/server/mcpServer.ts b/src/server/mcpServer.ts index feb7b41..7948987 100644 --- a/src/server/mcpServer.ts +++ b/src/server/mcpServer.ts @@ -1,21 +1,21 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; - import { Logger } from '../utils/logger.util'; -import { config } from '../utils/config.util'; +import configLoader from '../config/config-loader'; import { PACKAGE_NAME, VERSION } from '../utils/constants.util'; -import posthogLlmResources from '../resources/posthog-llm.resource.js'; import posthogLlmTools from '../tools/posthog-llm.tool.js'; +import openTelemetryTools from '../tools/opentelemetry-llm.tool.js'; export function createServer() { const serverLogger = Logger.forContext('utils/server.util.ts', 'getServer'); // Load configuration serverLogger.info('Starting MCP server initialization...'); - config.load(); + configLoader.load(); - if (config.getBoolean('DEBUG')) { + const commonConfig = configLoader.getCommonConfig(); + if (commonConfig.debug) { serverLogger.debug('Debug mode enabled'); } @@ -25,11 +25,11 @@ export function createServer() { version: VERSION, }); - // Register tools and resources - serverLogger.info('Registering MCP tools and resources...'); + // Register tools + serverLogger.info('Registering MCP tools...'); posthogLlmTools.registerTools(server); - posthogLlmResources.registerResources(server); - serverLogger.debug('All tools and resources registered'); + openTelemetryTools.registerTools(server); + serverLogger.debug('All tools registered'); return server; } diff --git a/src/services/opentelemetry-llm.service.ts b/src/services/opentelemetry-llm.service.ts new file mode 100644 index 0000000..48cdf38 --- /dev/null +++ b/src/services/opentelemetry-llm.service.ts @@ -0,0 +1,290 @@ +import { + MeterProvider, + PeriodicExportingMetricReader, +} from '@opentelemetry/sdk-metrics'; +import { NodeSDK } from '@opentelemetry/sdk-node'; +import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'; +import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { resourceFromAttributes } from '@opentelemetry/resources'; +import { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, + SEMRESATTRS_DEPLOYMENT_ENVIRONMENT, +} from '@opentelemetry/semantic-conventions'; +import { Logger } from '../utils/logger.util.js'; +import type { OpenTelemetryConfigType } from '../config/opentelemetry-llm.schema.js'; +import { OpenTelemetryLlmInputSchemaType } from '../types/opentelemetry-llm.types.js'; +import { + Meter, + Counter, + Histogram, + UpDownCounter, + metrics, +} from '@opentelemetry/api'; +import { trace, Tracer, SpanStatusCode } from '@opentelemetry/api'; +import configLoader from '../config/config-loader'; + +/** + * OpenTelemetry service for LLM observability + */ +export class OpenTelemetryService { + private static instance: OpenTelemetryService; + private sdk: NodeSDK | null = null; + private meterProvider: MeterProvider | null = null; + private tracer: Tracer; + private meter: Meter; + private config: OpenTelemetryConfigType; + + // Metrics + private requestCounter: Counter; + private tokenCounter: Counter; + private latencyHistogram: Histogram; + private activeRequests: UpDownCounter; + + private constructor() { + this.config = configLoader.getOpenTelemetryConfig(); + this.tracer = trace.getTracer('llm-observability-mcp'); + this.meter = this.initializeMetrics(); + + // Initialize metrics + this.requestCounter = this.meter.createCounter('llm.requests.total', { + description: 'Total number of LLM requests', + }); + + this.tokenCounter = this.meter.createCounter('llm.tokens.total', { + description: 'Total number of tokens processed', + }); + + this.latencyHistogram = this.meter.createHistogram( + 'llm.latency.duration', + { + description: 'Duration of LLM requests in milliseconds', + unit: 'ms', + }, + ); + + this.activeRequests = this.meter.createUpDownCounter( + 'llm.requests.active', + { + description: 'Number of active LLM requests', + }, + ); + } + + /** + * Get singleton instance + */ + public static getInstance(): OpenTelemetryService { + if (!OpenTelemetryService.instance) { + OpenTelemetryService.instance = new OpenTelemetryService(); + } + return OpenTelemetryService.instance; + } + + /** + * Initialize OpenTelemetry SDK + */ + public initialize(): void { + const logger = Logger.forContext('opentelemetry.service', 'initialize'); + + if (this.sdk) { + logger.warn('OpenTelemetry SDK already initialized'); + return; + } + + try { + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: this.config.serviceName, + [ATTR_SERVICE_VERSION]: this.config.serviceVersion, + [SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: this.config.environment, + debug: this.config.debug, + logLevel: this.config.logLevel, + }); + + // Configure metric exporter + const metricExporter = this.config.metricsEndpoint + ? new OTLPMetricExporter({ + url: this.config.metricsEndpoint, + headers: this.config.headers, + }) + : undefined; + + // Configure trace exporter + const traceExporter = this.config.tracesEndpoint + ? new OTLPTraceExporter({ + url: this.config.tracesEndpoint, + headers: this.config.headers, + }) + : undefined; + + // Initialize meter provider + if (metricExporter) { + this.meterProvider = new MeterProvider({ + resource, + readers: [ + new PeriodicExportingMetricReader({ + exporter: metricExporter, + exportIntervalMillis: + this.config.exportIntervalMillis, + exportTimeoutMillis: + this.config.exportTimeoutMillis, + }), + ], + }); + } + + // Initialize SDK + this.sdk = new NodeSDK({ + resource, + spanProcessor: traceExporter + ? new BatchSpanProcessor(traceExporter) + : undefined, + }); + + this.sdk.start(); + logger.info('OpenTelemetry SDK initialized successfully'); + } catch (error) { + logger.error('Failed to initialize OpenTelemetry SDK', error); + throw error; + } + } + + /** + * Initialize metrics + */ + private initializeMetrics(): Meter { + if (this.meterProvider) { + return this.meterProvider.getMeter('llm-observability-mcp'); + } + return metrics.getMeter('llm-observability-mcp'); + } + + /** + * Record LLM request metrics and traces + */ + public recordLlmRequest(data: OpenTelemetryLlmInputSchemaType): void { + const labels = { + model: data.model, + provider: data.provider, + userId: data.userId, + operationName: data.operationName || 'generate', + status: data.error ? 'error' : 'success', + errorType: data.errorType, + mcpToolsUsed: data.mcpToolsUsed?.join(',') || '', + }; + + // Record metrics + this.requestCounter.add(1, labels); + + if (data.inputTokens || data.outputTokens) { + const totalTokens = + (data.inputTokens || 0) + (data.outputTokens || 0); + this.tokenCounter.add(totalTokens, { + ...labels, + type: 'total', + }); + + if (data.inputTokens) { + this.tokenCounter.add(data.inputTokens, { + ...labels, + type: 'input', + }); + } + + if (data.outputTokens) { + this.tokenCounter.add(data.outputTokens, { + ...labels, + type: 'output', + }); + } + } + + if (data.latency) { + this.latencyHistogram.record(data.latency * 1000, labels); + } + + // Create trace + this.createLlmTrace(data); + } + + /** + * Create distributed trace for LLM request + */ + private createLlmTrace(data: OpenTelemetryLlmInputSchemaType): void { + const spanName = data.operationName || 'llm.generate'; + const span = this.tracer.startSpan(spanName, { + attributes: { + 'llm.model': data.model, + 'llm.provider': data.provider, + 'llm.user_id': data.userId, + 'llm.operation': data.operationName || 'generate', + 'llm.input_tokens': data.inputTokens, + 'llm.output_tokens': data.outputTokens, + 'llm.total_tokens': + (data.inputTokens || 0) + (data.outputTokens || 0), + 'llm.latency_ms': data.latency + ? data.latency * 1000 + : undefined, + 'llm.http_status': data.httpStatus, + 'llm.base_url': data.baseUrl, + 'llm.error': data.error, + 'llm.error_type': data.errorType, + 'llm.input': + typeof data.input === 'string' + ? data.input + : JSON.stringify(data.input), + 'llm.output': + typeof data.outputChoices === 'string' + ? data.outputChoices + : JSON.stringify(data.outputChoices), + 'llm.mcp_tools_used': data.mcpToolsUsed?.join(','), + }, + }); + + if (data.traceId) { + span.setAttribute('trace.id', data.traceId); + } + + if (data.error) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: data.error, + }); + } else { + span.setStatus({ code: SpanStatusCode.OK }); + } + + span.end(); + } + + /** + * Record active request count + */ + public incrementActiveRequests(): void { + this.activeRequests.add(1); + } + + /** + * Decrement active request count + */ + public decrementActiveRequests(): void { + this.activeRequests.add(-1); + } + + /** + * Shutdown OpenTelemetry SDK + */ + public async shutdown(): Promise { + const logger = Logger.forContext('opentelemetry.service', 'shutdown'); + + if (this.sdk) { + try { + await this.sdk.shutdown(); + logger.info('OpenTelemetry SDK shutdown successfully'); + } catch (error) { + logger.error('Error shutting down OpenTelemetry SDK', error); + } + } + } +} diff --git a/src/services/posthog-llm.service.ts b/src/services/posthog-llm.service.ts index 0953398..8ea8000 100644 --- a/src/services/posthog-llm.service.ts +++ b/src/services/posthog-llm.service.ts @@ -1,18 +1,18 @@ -import { Logger } from '../utils/logger.util.js'; +import { Logger } from '../utils/logger.util'; import { PostHog } from 'posthog-node'; -import { config } from '../utils/config.util.js'; +import configLoader from '../config/config-loader'; // Ensure configuration is loaded before accessing environment variables -config.load(); +configLoader.load(); const logger = Logger.forContext('services/posthog-llm.service.ts'); -const posthogApiKey = config.get('POSTHOG_API_KEY'); +const posthogConfig = configLoader.getPosthogConfig(); let posthogClient: PostHog | null = null; -if (posthogApiKey) { - posthogClient = new PostHog(posthogApiKey, { - host: config.get('POSTHOG_HOST') || 'https://app.posthog.com', +if (posthogConfig.apiKey) { + posthogClient = new PostHog(posthogConfig.apiKey, { + host: posthogConfig.host, }); } else { logger.warn('POSTHOG_API_KEY is not set. PostHog client not initialized.'); diff --git a/src/tools/opentelemetry-llm.tool.ts b/src/tools/opentelemetry-llm.tool.ts new file mode 100644 index 0000000..9ac1495 --- /dev/null +++ b/src/tools/opentelemetry-llm.tool.ts @@ -0,0 +1,77 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { Logger } from '../utils/logger.util.js'; +import { formatErrorForMcpTool } from '../utils/error.util.js'; +import { OpenTelemetryController } from '../controllers/opentelemetry-llm.controller.js'; +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { + OpenTelemetryLlmInputSchema, + OpenTelemetryLlmInputSchemaType, +} from '../types/opentelemetry-llm.types.js'; + +/** + * @function captureOpenTelemetryLlmObservability + * @description MCP Tool handler to capture LLM observability events using OpenTelemetry. + * It records metrics, traces, and spans for LLM requests that can be sent to any OpenTelemetry-compatible backend. + * @param {OpenTelemetryLlmInputSchemaType} args - Arguments provided to the tool. + * @returns {Promise} Formatted response for the MCP. + * @throws {McpError} Formatted error if the controller or service layer encounters an issue. + */ +async function captureOpenTelemetryLlmObservability( + args: OpenTelemetryLlmInputSchemaType, +): Promise { + const methodLogger = Logger.forContext( + 'opentelemetry.tool', + 'captureOpenTelemetryLlmObservability', + ); + methodLogger.debug('Capture LLM Observability with OpenTelemetry...', args); + + try { + // Parse and validate arguments + const validatedArgs = OpenTelemetryLlmInputSchema.parse(args); + + // Pass validated args to the controller + const result = await OpenTelemetryController.capture(validatedArgs); + methodLogger.debug('Got response from controller', result); + + // Format the response for the MCP tool + return { + content: [ + { + type: 'text' as const, + text: result.content, + }, + ], + }; + } catch (error) { + methodLogger.error( + 'Error tracking LLM generation with OpenTelemetry', + error, + ); + return formatErrorForMcpTool(error); + } +} + +/** + * @function registerTools + * @description Registers the OpenTelemetry LLM observability tool with the MCP server. + * + * @param {McpServer} server - The MCP server instance. + */ +function registerTools(server: McpServer) { + const methodLogger = Logger.forContext( + 'opentelemetry.tool', + 'registerTools', + ); + methodLogger.debug('Registering OpenTelemetry LLM observability tools...'); + + server.tool( + 'llm_observability_otel', + `Captures LLM usage using OpenTelemetry for observability, including requests, responses, and performance metrics. Works with any OpenTelemetry-compatible backend like Jaeger, New Relic, Grafana, etc.`, + OpenTelemetryLlmInputSchema.shape, + captureOpenTelemetryLlmObservability, + ); + + methodLogger.debug('Successfully registered llm_observability_otel tool.'); +} + +export default { registerTools }; diff --git a/src/tools/posthog-llm.tool.ts b/src/tools/posthog-llm.tool.ts index 1e5bb11..3cc6968 100644 --- a/src/tools/posthog-llm.tool.ts +++ b/src/tools/posthog-llm.tool.ts @@ -1,23 +1,25 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { Logger } from '../utils/logger.util.js'; import { formatErrorForMcpTool } from '../utils/error.util.js'; -import posthogLlmController from '../controllers/posthog-llm.controller.js'; +import { PostHogController } from '../controllers/posthog-llm.controller.js'; import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { - GetToolInputSchema, - GetToolInputSchemaType, -} from './posthog-llm.types.js'; + PostHogLlmInputSchemaType, + PostHogLlmPropertiesApiSchemaType, + PostHogLlmPropertiesPayloadSchema, + PostHogLlmPropertiesPayloadSchemaType, +} from '../types/posthog-llm.types.js'; /** * @function capturePosthogLlmObservability * @description MCP Tool handler to capture LLM observability events in PostHog. * It calls the posthogLlmController to track the data and formats the response for the MCP. - * @param {GetToolInputSchemaType} args - Arguments provided to the tool. + * @param {PostHogLlmInputSchemaType} args - Arguments provided to the tool. * @returns {Promise} Formatted response for the MCP. * @throws {McpError} Formatted error if the controller or service layer encounters an issue. */ async function capturePosthogLlmObservability( - args: GetToolInputSchemaType, + args: PostHogLlmPropertiesPayloadSchemaType, ): Promise { const methodLogger = Logger.forContext( 'tools/posthog-llm.tool.ts', @@ -26,15 +28,16 @@ async function capturePosthogLlmObservability( methodLogger.debug(`Capture LLM Observability in PostHog...`, args); try { - const trackArgs = GetToolInputSchema.parse(args); + const trackArgs = PostHogLlmPropertiesPayloadSchema.parse(args); - const posthogProperties: Record = { + const posthogProperties: PostHogLlmPropertiesApiSchemaType = { + userId: trackArgs.userId, $ai_model: trackArgs.model, $ai_provider: trackArgs.provider, }; const toPostHogKey: Partial< - Record + Record > = { input: '$ai_input', outputChoices: '$ai_output_choices', @@ -47,22 +50,27 @@ async function capturePosthogLlmObservability( }; for (const key of Object.keys(toPostHogKey) as Array< - keyof GetToolInputSchemaType + keyof PostHogLlmPropertiesPayloadSchemaType >) { if (trackArgs[key] !== undefined) { const posthogKey = toPostHogKey[key]; if (posthogKey) { - posthogProperties[posthogKey] = trackArgs[key]; + // Type assertion to satisfy TS: posthogKey is a key of posthogProperties + (posthogProperties as Record)[posthogKey] = + trackArgs[key]; } } } - // Pass validated args to the controller - const result = await posthogLlmController.capture({ + // validated input for the controller + const posthogInput: PostHogLlmInputSchemaType = { eventName: '$ai_generation', distinctId: trackArgs.userId, properties: posthogProperties, - }); + }; + + // Pass validated args to the controller + const result = await PostHogController.capture(posthogInput); methodLogger.debug(`Got the response from the controller`, result); // Format the response for the MCP tool @@ -82,7 +90,7 @@ async function capturePosthogLlmObservability( /** * @function registerTools - * @description Registers the PostHog LLM observability tool ('capture_llm_observability') with the MCP server. + * @description Registers the PostHog LLM observability tool ('llm_observability_posthog') with the MCP server. * * @param {McpServer} server - The MCP server instance. */ @@ -94,14 +102,14 @@ function registerTools(server: McpServer) { methodLogger.debug(`Registering PostHog LLM observability tools...`); server.tool( - 'capture_llm_observability', + 'llm_observability_posthog', `Captures LLM usage in PostHog for observability, including requests, responses, and performance metrics`, - GetToolInputSchema.shape, + PostHogLlmPropertiesPayloadSchema.shape, capturePosthogLlmObservability, ); methodLogger.debug( - 'Successfully registered capture_llm_observability tool.', + 'Successfully registered llm_observability_posthog tool.', ); } diff --git a/src/tools/posthog-llm.types.ts b/src/tools/posthog-llm.types.ts deleted file mode 100644 index 044427d..0000000 --- a/src/tools/posthog-llm.types.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { z } from 'zod'; - -/** - * Zod schema for the PostHog LLM observability tool arguments. - */ -export const GetToolInputSchema = z.object({ - userId: z.string().describe('The distinct ID of the user'), - traceId: z.string().optional().describe('The trace ID to group AI events'), - model: z.string().describe('The model used (e.g., gpt-4, claude-3, etc.)'), - provider: z - .string() - .describe('The LLM provider (e.g., openai, anthropic, etc.)'), - input: z - .any() - .optional() - .describe('The input to the LLM (messages, prompt, etc.)'), - outputChoices: z.any().optional().describe('The output from the LLM'), - inputTokens: z - .number() - .optional() - .describe('The number of tokens in the input'), - outputTokens: z - .number() - .optional() - .describe('The number of tokens in the output'), - latency: z - .number() - .optional() - .describe('The latency of the LLM call in seconds'), - httpStatus: z - .number() - .optional() - .describe('The HTTP status code of the LLM call'), - baseUrl: z.string().optional().describe('The base URL of the LLM API'), -}); - -/** - * TypeScript type inferred from the GetToolInputSchema Zod schema. - * This represents the optional arguments passed to the tool handler and controller. - */ -export type GetToolInputSchemaType = z.infer; diff --git a/src/types/opentelemetry-llm.types.ts b/src/types/opentelemetry-llm.types.ts new file mode 100644 index 0000000..c7c5cdb --- /dev/null +++ b/src/types/opentelemetry-llm.types.ts @@ -0,0 +1,168 @@ +import { z } from 'zod'; + +/** + * Zod schema for the OpenTelemetry LLM observability tool arguments + */ +export const OpenTelemetryLlmInputSchema = z.object({ + // Required fields + userId: z.string().describe('The distinct ID of the user'), + model: z.string().describe('The model used (e.g., gpt-4, claude-3, etc.)'), + provider: z + .string() + .describe('The LLM provider (e.g., openai, anthropic, etc.)'), + + // Optional fields + traceId: z.string().optional().describe('The trace ID to group AI events'), + input: z + .any() + .optional() + .describe('The input to the LLM (messages, prompt, etc.)'), + outputChoices: z.any().optional().describe('The output from the LLM'), + inputTokens: z + .number() + .optional() + .describe('The number of tokens in the input'), + outputTokens: z + .number() + .optional() + .describe('The number of tokens in the output'), + latency: z + .number() + .optional() + .describe('The latency of the LLM call in seconds'), + httpStatus: z + .number() + .optional() + .describe('The HTTP status code of the LLM call'), + baseUrl: z.string().optional().describe('The base URL of the LLM API'), + + // OpenTelemetry specific fields + operationName: z + .string() + .optional() + .describe('The name of the operation being performed'), + error: z + .string() + .optional() + .describe('Error message if the request failed'), + errorType: z + .string() + .optional() + .describe('Type of error (e.g., rate_limit, timeout, etc.)'), + mcpToolsUsed: z + .array(z.string()) + .optional() + .describe('List of MCP tools used during the request'), +}); + +/** + * TypeScript type inferred from the OpenTelemetryLlmInputSchema Zod schema + */ +export type OpenTelemetryLlmInputSchemaType = z.infer< + typeof OpenTelemetryLlmInputSchema +>; + +/** + * Configuration for OpenTelemetry exporters + */ +export interface OpenTelemetryExporterConfig { + url: string; + headers?: Record; + timeout?: number; +} + +/** + * Supported OpenTelemetry backends + */ +export type OpenTelemetryBackend = + | 'jaeger' + | 'newrelic' + | 'grafana' + | 'datadog' + | 'honeycomb' + | 'lightstep' + | 'custom'; + +/** + * Pre-configured settings for popular OpenTelemetry backends + */ +export const OpenTelemetryBackendConfigs: Record< + OpenTelemetryBackend, + Partial +> = { + jaeger: { + url: 'http://localhost:4318/v1/traces', + headers: {}, + }, + newrelic: { + url: 'https://otlp.nr-data.net:4318/v1/traces', + headers: { + 'api-key': process.env.NEW_RELIC_LICENSE_KEY || '', + }, + }, + grafana: { + url: 'https://otlp-gateway-prod-us-central-0.grafana.net/otlp/v1/traces', + headers: { + Authorization: + process.env.GRAFANA_INSTANCE_ID && process.env.GRAFANA_API_KEY + ? `Basic ${Buffer.from( + `${process.env.GRAFANA_INSTANCE_ID}:${process.env.GRAFANA_API_KEY}`, + ).toString('base64')}` + : '', + }, + }, + datadog: { + url: 'https://api.datadoghq.com/api/v2/series', + headers: { + 'DD-API-KEY': process.env.DD_API_KEY || '', + }, + }, + honeycomb: { + url: 'https://api.honeycomb.io/v1/traces', + headers: { + 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY || '', + }, + }, + lightstep: { + url: 'https://ingest.lightstep.com:443/api/v2/otel/trace', + headers: { + 'lightstep-access-token': process.env.LIGHTSTEP_ACCESS_TOKEN || '', + }, + }, + custom: { + url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318', + headers: {}, + }, +}; + +/** + * Metric names used by the OpenTelemetry service + */ +export const OpenTelemetryMetrics = { + REQUESTS_TOTAL: 'llm.requests.total', + TOKENS_TOTAL: 'llm.tokens.total', + LATENCY_DURATION: 'llm.latency.duration', + REQUESTS_ACTIVE: 'llm.requests.active', +} as const; + +/** + * Trace attribute names used by the OpenTelemetry service + */ +export const OpenTelemetryAttributes = { + LLM_MODEL: 'llm.model', + LLM_PROVIDER: 'llm.provider', + LLM_USER_ID: 'llm.user_id', + LLM_OPERATION: 'llm.operation', + LLM_INPUT_TOKENS: 'llm.input_tokens', + LLM_OUTPUT_TOKENS: 'llm.output_tokens', + LLM_TOTAL_TOKENS: 'llm.total_tokens', + LLM_LATENCY_MS: 'llm.latency_ms', + LLM_HTTP_STATUS: 'llm.http_status', + LLM_BASE_URL: 'llm.base_url', + LLM_ERROR: 'llm.error', + LLM_ERROR_TYPE: 'llm.error_type', + LLM_INPUT: 'llm.input', + LLM_OUTPUT: 'llm.output', + LLM_MCP_TOOLS_USED: 'llm.mcp_tools_used', + TRACE_ID: 'trace.id', +} as const; diff --git a/src/types/posthog-llm.types.ts b/src/types/posthog-llm.types.ts new file mode 100644 index 0000000..5691621 --- /dev/null +++ b/src/types/posthog-llm.types.ts @@ -0,0 +1,93 @@ +import { z } from 'zod'; + +/** + * Zod schema for the PostHog LLM observability tool properties (payload). + */ +export const PostHogLlmPropertiesPayloadSchema = z.object({ + userId: z.string().describe('The distinct ID of the user'), + traceId: z.string().optional().describe('The trace ID to group AI events'), + model: z.string().describe('The model used (e.g., gpt-4, claude-3, etc.)'), + provider: z + .string() + .describe('The LLM provider (e.g., openai, anthropic, etc.)'), + input: z + .any() + .optional() + .describe('The input to the LLM (messages, prompt, etc.)'), + outputChoices: z.any().optional().describe('The output from the LLM'), + inputTokens: z + .number() + .optional() + .describe('The number of tokens in the input'), + outputTokens: z + .number() + .optional() + .describe('The number of tokens in the output'), + latency: z + .number() + .optional() + .describe('The latency of the LLM call in seconds'), + httpStatus: z + .number() + .optional() + .describe('The HTTP status code of the LLM call'), + baseUrl: z.string().optional().describe('The base URL of the LLM API'), +}); + +export type PostHogLlmPropertiesPayloadSchemaType = z.infer< + typeof PostHogLlmPropertiesPayloadSchema +>; + +/** + * Zod schema for the PostHog LLM observability tool properties (api payload). + */ +export const PostHogLlmPropertiesApiSchema = z.object({ + userId: z.string().describe('The distinct ID of the user'), + $ai_trace_id: z + .string() + .optional() + .describe('The trace ID to group AI events'), + $ai_model: z + .string() + .describe('The model used (e.g., gpt-4, claude-3, etc.)'), + $ai_provider: z + .string() + .describe('The LLM provider (e.g., openai, anthropic, etc.)'), + $ai_input: z + .any() + .optional() + .describe('The input to the LLM (messages, prompt, etc.)'), + $ai_output_choices: z.any().optional().describe('The output from the LLM'), + $ai_input_tokens: z + .number() + .optional() + .describe('The number of tokens in the input'), + $ai_output_tokens: z + .number() + .optional() + .describe('The number of tokens in the output'), + $ai_latency: z + .number() + .optional() + .describe('The latency of the LLM call in seconds'), + $ai_http_status: z + .number() + .optional() + .describe('The HTTP status code of the LLM call'), + $ai_base_url: z.string().optional().describe('The base URL of the LLM API'), +}); + +export type PostHogLlmPropertiesApiSchemaType = z.infer< + typeof PostHogLlmPropertiesApiSchema +>; + +/** + * Zod schema for the PostHog LLM observability tool arguments (top-level). + */ +export const PostHogLlmInputSchema = z.object({ + eventName: z.string().describe('The name of the event to capture.'), + distinctId: z.string().describe('The distinct ID of the user.'), + properties: PostHogLlmPropertiesApiSchema.describe('The event properties.'), +}); + +export type PostHogLlmInputSchemaType = z.infer; diff --git a/src/utils/cli.test.util.ts b/src/utils/cli.test.util.ts deleted file mode 100644 index cb566db..0000000 --- a/src/utils/cli.test.util.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { spawn } from 'child_process'; -import { join } from 'path'; - -/** - * Utility for testing CLI commands with real execution - */ -export class CliTestUtil { - /** - * Executes a CLI command and returns the result - * - * @param args - CLI arguments to pass to the command - * @param options - Test options - * @returns Promise with stdout, stderr, and exit code - */ - static async runCommand( - args: string[], - options: { - timeoutMs?: number; - env?: Record; - } = {}, - ): Promise<{ - stdout: string; - stderr: string; - exitCode: number; - }> { - // Default timeout of 30 seconds - const timeoutMs = options.timeoutMs || 30000; - - // CLI execution path - points to the built CLI script - const cliPath = join(process.cwd(), 'dist', 'index.js'); - - // Log what command we're about to run - console.log(`Running CLI command: node ${cliPath} ${args.join(' ')}`); - - return new Promise((resolve, reject) => { - // Set up timeout handler - const timeoutId = setTimeout(() => { - child.kill(); - reject(new Error(`CLI command timed out after ${timeoutMs}ms`)); - }, timeoutMs); - - // Capture stdout and stderr - let stdout = ''; - let stderr = ''; - - // Spawn the process with given arguments and enhanced environment - const child = spawn('node', [cliPath, ...args], { - env: { - ...process.env, - ...options.env, - DEBUG: 'true', // Enable debug logging - NODE_ENV: 'test', // Ensure tests are detected - }, - }); - - // Collect stdout data - child.stdout.on('data', (data) => { - const chunk = data.toString(); - stdout += chunk; - console.log(`STDOUT chunk: ${chunk.substring(0, 50)}...`); - }); - - // Collect stderr data - child.stderr.on('data', (data) => { - const chunk = data.toString(); - stderr += chunk; - console.log(`STDERR chunk: ${chunk.substring(0, 50)}...`); - }); - - // Handle process completion - child.on('close', (exitCode) => { - clearTimeout(timeoutId); - console.log(`Command completed with exit code: ${exitCode}`); - console.log(`Total STDOUT length: ${stdout.length} chars`); - - // Get the non-debug output for debugging purposes - const nonDebugOutput = stdout - .split('\n') - .filter((line) => !line.match(/^\[\d{2}:\d{2}:\d{2}\]/)) - .join('\n'); - - console.log( - `Non-debug output length: ${nonDebugOutput.length} chars`, - ); - console.log(`STDOUT excerpt: ${stdout.substring(0, 100)}...`); - console.log( - `Filtered excerpt: ${nonDebugOutput.substring(0, 100)}...`, - ); - - resolve({ - stdout, - stderr, - exitCode: exitCode ?? 0, - }); - }); - - // Handle process errors - child.on('error', (err) => { - clearTimeout(timeoutId); - console.error(`Command error: ${err.message}`); - reject(err); - }); - }); - } - - /** - * Validates that stdout contains expected strings/patterns - */ - static validateOutputContains( - output: string, - expectedPatterns: (string | RegExp)[], - ): void { - // Filter out debug log lines for cleaner validation - const cleanOutput = output - .split('\n') - .filter((line) => !line.match(/^\[\d{2}:\d{2}:\d{2}\]/)) - .join('\n'); - - console.log('==== Cleaned output for validation ===='); - console.log(cleanOutput); - console.log('======================================='); - - for (const pattern of expectedPatterns) { - if (typeof pattern === 'string') { - expect(cleanOutput).toContain(pattern); - } else { - expect(cleanOutput).toMatch(pattern); - } - } - } - - /** - * Validates Markdown output format - */ - static validateMarkdownOutput(output: string): void { - // Filter out debug log lines for cleaner validation - const cleanOutput = output - .split('\n') - .filter((line) => !line.match(/^\[\d{2}:\d{2}:\d{2}\]/)) - .join('\n'); - - // Check for Markdown heading - expect(cleanOutput).toMatch(/^#\s.+/m); - - // Check for markdown formatting elements like bold text, lists, etc. - const markdownElements = [ - /\*\*.+\*\*/, // Bold text - /-\s.+/, // List items - /\|.+\|.+\|/, // Table rows - /\[.+\]\(.+\)/, // Links - ]; - - expect( - markdownElements.some((pattern) => pattern.test(cleanOutput)), - ).toBe(true); - } -} diff --git a/src/utils/config.util.test.ts b/src/utils/config.util.test.ts deleted file mode 100644 index 8e70482..0000000 --- a/src/utils/config.util.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { - ErrorType, - McpError, - createApiError, - createAuthMissingError, - createAuthInvalidError, - createUnexpectedError, - ensureMcpError, - formatErrorForMcpTool, - formatErrorForMcpResource, -} from './error.util.js'; - -describe('Error Utility', () => { - describe('McpError', () => { - it('should create an error with the correct properties', () => { - const error = new McpError('Test error', ErrorType.API_ERROR, 404); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(McpError); - expect(error.message).toBe('Test error'); - expect(error.type).toBe(ErrorType.API_ERROR); - expect(error.statusCode).toBe(404); - expect(error.name).toBe('McpError'); - }); - }); - - describe('Error Factory Functions', () => { - it('should create auth missing error', () => { - const error = createAuthMissingError(); - - expect(error).toBeInstanceOf(McpError); - expect(error.type).toBe(ErrorType.AUTH_MISSING); - expect(error.message).toBe( - 'Authentication credentials are missing', - ); - }); - - it('should create auth invalid error', () => { - const error = createAuthInvalidError('Invalid token'); - - expect(error).toBeInstanceOf(McpError); - expect(error.type).toBe(ErrorType.AUTH_INVALID); - expect(error.statusCode).toBe(401); - expect(error.message).toBe('Invalid token'); - }); - - it('should create API error', () => { - const originalError = new Error('Original error'); - const error = createApiError('API failed', 500, originalError); - - expect(error).toBeInstanceOf(McpError); - expect(error.type).toBe(ErrorType.API_ERROR); - expect(error.statusCode).toBe(500); - expect(error.message).toBe('API failed'); - expect(error.originalError).toBe(originalError); - }); - - it('should create unexpected error', () => { - const error = createUnexpectedError(); - - expect(error).toBeInstanceOf(McpError); - expect(error.type).toBe(ErrorType.UNEXPECTED_ERROR); - expect(error.message).toBe('An unexpected error occurred'); - }); - }); - - describe('ensureMcpError', () => { - it('should return the same error if it is already an McpError', () => { - const originalError = createApiError('Original error'); - const error = ensureMcpError(originalError); - - expect(error).toBe(originalError); - }); - - it('should wrap a standard Error', () => { - const originalError = new Error('Standard error'); - const error = ensureMcpError(originalError); - - expect(error).toBeInstanceOf(McpError); - expect(error.type).toBe(ErrorType.UNEXPECTED_ERROR); - expect(error.message).toBe('Standard error'); - expect(error.originalError).toBe(originalError); - }); - - it('should handle non-Error objects', () => { - const error = ensureMcpError('String error'); - - expect(error).toBeInstanceOf(McpError); - expect(error.type).toBe(ErrorType.UNEXPECTED_ERROR); - expect(error.message).toBe('String error'); - }); - }); - - describe('formatErrorForMcpTool', () => { - it('should format an error for MCP tool response', () => { - const error = createApiError('API error'); - const response = formatErrorForMcpTool(error); - - expect(response).toHaveProperty('content'); - expect(response.content).toHaveLength(1); - expect(response.content[0]).toHaveProperty('type', 'text'); - expect(response.content[0]).toHaveProperty( - 'text', - 'Error: API error', - ); - }); - }); - - describe('formatErrorForMcpResource', () => { - it('should format an error for MCP resource response', () => { - const error = createApiError('API error'); - const response = formatErrorForMcpResource(error, 'test://uri'); - - expect(response).toHaveProperty('contents'); - expect(response.contents).toHaveLength(1); - expect(response.contents[0]).toHaveProperty('uri', 'test://uri'); - expect(response.contents[0]).toHaveProperty( - 'text', - 'Error: API error', - ); - expect(response.contents[0]).toHaveProperty( - 'mimeType', - 'text/plain', - ); - expect(response.contents[0]).toHaveProperty( - 'description', - 'Error: API_ERROR', - ); - }); - }); -}); diff --git a/src/utils/config.util.ts b/src/utils/config.util.ts deleted file mode 100644 index 8bca54d..0000000 --- a/src/utils/config.util.ts +++ /dev/null @@ -1,173 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { Logger } from './logger.util.js'; -import dotenv from 'dotenv'; -import os from 'os'; - -/** - * Configuration loader that handles multiple sources with priority: - * 1. Direct ENV pass (process.env) - * 2. .env file in project root - * 3. Global config file at $HOME/.mcp/configs.json - */ -class ConfigLoader { - private packageName: string; - private configLoaded: boolean = false; - - /** - * Create a new ConfigLoader instance - * @param packageName The package name to use for global config lookup - */ - constructor(packageName: string) { - this.packageName = packageName; - } - - /** - * Load configuration from all sources with proper priority - */ - load(): void { - const methodLogger = Logger.forContext('utils/config.util.ts', 'load'); - - if (this.configLoaded) { - methodLogger.debug('Configuration already loaded, skipping'); - return; - } - - methodLogger.debug('Loading configuration...'); - - // Priority 3: Load from global config file - this.loadFromGlobalConfig(); - - // Priority 2: Load from .env file - this.loadFromEnvFile(); - - // Priority 1: Direct ENV pass is already in process.env - // No need to do anything as it already has highest priority - - this.configLoaded = true; - methodLogger.debug('Configuration loaded successfully'); - } - - /** - * Load configuration from .env file in project root - */ - private loadFromEnvFile(): void { - const methodLogger = Logger.forContext( - 'utils/config.util.ts', - 'loadFromEnvFile', - ); - - try { - const result = dotenv.config(); - if (result.error) { - methodLogger.debug('No .env file found or error reading it'); - return; - } - methodLogger.debug('Loaded configuration from .env file'); - } catch (error) { - methodLogger.error('Error loading .env file', error); - } - } - - /** - * Load configuration from global config file at $HOME/.mcp/configs.json - */ - private loadFromGlobalConfig(): void { - const methodLogger = Logger.forContext( - 'utils/config.util.ts', - 'loadFromGlobalConfig', - ); - - try { - const homedir = os.homedir(); - const globalConfigPath = path.join(homedir, '.mcp', 'configs.json'); - - if (!fs.existsSync(globalConfigPath)) { - methodLogger.debug('Global config file not found'); - return; - } - - const configContent = fs.readFileSync(globalConfigPath, 'utf8'); - const config = JSON.parse(configContent); - - // Determine the potential keys for the current package - const shortKey = 'llm-observability-mcp'; // Project-specific short key - const fullPackageName = this.packageName; // e.g., '@sfiorini/llm-observability-mcp' - const unscopedPackageName = - fullPackageName.split('/')[1] || fullPackageName; // e.g., 'llm-observability-mcp' - - const potentialKeys = [ - shortKey, - fullPackageName, - unscopedPackageName, - ]; - let foundConfigSection: { - environments?: Record; - } | null = null; - let usedKey: string | null = null; - - for (const key of potentialKeys) { - if ( - config[key] && - typeof config[key] === 'object' && - config[key].environments - ) { - foundConfigSection = config[key]; - usedKey = key; - methodLogger.debug(`Found configuration using key: ${key}`); - break; // Stop once found - } - } - - if (!foundConfigSection || !foundConfigSection.environments) { - methodLogger.debug( - `No configuration found for ${ - this.packageName - } using keys: ${potentialKeys.join(', ')}`, - ); - return; - } - - const environments = foundConfigSection.environments; - for (const [key, value] of Object.entries(environments)) { - // Only set if not already defined in process.env - if (process.env[key] === undefined) { - process.env[key] = String(value); - } - } - - methodLogger.debug( - `Loaded configuration from global config file using key: ${usedKey}`, - ); - } catch (error) { - methodLogger.error('Error loading global config file', error); - } - } - - /** - * Get a configuration value - * @param key The configuration key - * @param defaultValue The default value if the key is not found - * @returns The configuration value or the default value - */ - get(key: string, defaultValue?: string): string | undefined { - return process.env[key] || defaultValue; - } - - /** - * Get a boolean configuration value - * @param key The configuration key - * @param defaultValue The default value if the key is not found - * @returns The boolean configuration value or the default value - */ - getBoolean(key: string, defaultValue: boolean = false): boolean { - const value = this.get(key); - if (value === undefined) { - return defaultValue; - } - return value.toLowerCase() === 'true'; - } -} - -// Create and export a singleton instance with the package name from package.json -export const config = new ConfigLoader('@sfiorini/llm-observability-mcp'); diff --git a/src/utils/error-handler.util.test.ts b/src/utils/error-handler.util.test.ts deleted file mode 100644 index 8ffd4cc..0000000 --- a/src/utils/error-handler.util.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - ErrorCode, - detectErrorType, - buildErrorContext, -} from './error-handler.util.js'; -import { createApiError, McpError, ErrorType } from './error.util.js'; - -describe('error-handler.util', () => { - describe('detectErrorType', () => { - it('should detect network errors', () => { - const networkErrors = [ - 'network error occurred', - 'fetch failed with error', - 'ECONNREFUSED on 127.0.0.1:8080', - 'ENOTFOUND api.example.com', - 'Failed to fetch data from server', - 'Network request failed', - ]; - - networkErrors.forEach((msg) => { - const { code, statusCode } = detectErrorType(new Error(msg)); - expect(code).toBe(ErrorCode.NETWORK_ERROR); - expect(statusCode).toBe(500); - }); - }); - - it('should detect rate limit errors', () => { - const rateLimitErrors = [ - 'rate limit exceeded', - 'too many requests', - new McpError('API error', ErrorType.API_ERROR, 429), - ]; - - rateLimitErrors.forEach((error) => { - const { code, statusCode } = detectErrorType(error); - expect(code).toBe(ErrorCode.RATE_LIMIT_ERROR); - expect(statusCode).toBe(429); - }); - }); - - it('should detect not found errors', () => { - const notFoundErrors = [ - 'resource not found', - 'entity does not exist', - new McpError('Not found', ErrorType.API_ERROR, 404), - ]; - - notFoundErrors.forEach((error) => { - const { code } = detectErrorType(error); - expect(code).toBe(ErrorCode.NOT_FOUND); - }); - }); - - it('should detect access denied errors', () => { - const accessDeniedErrors = [ - 'access denied', - 'permission denied', - 'not authorized to access', - 'authentication required', - new McpError('Forbidden', ErrorType.API_ERROR, 403), - new McpError('Unauthorized', ErrorType.API_ERROR, 401), - ]; - - accessDeniedErrors.forEach((error) => { - const { code } = detectErrorType(error); - expect(code).toBe(ErrorCode.ACCESS_DENIED); - }); - }); - - it('should default to unexpected error when no patterns match', () => { - const { code, statusCode } = detectErrorType( - new Error('some random error'), - ); - expect(code).toBe(ErrorCode.UNEXPECTED_ERROR); - expect(statusCode).toBe(500); - }); - }); - - describe('buildErrorContext', () => { - it('should build a context object with all parameters', () => { - const context = buildErrorContext( - 'User', - 'create', - 'controllers/user.controller.ts@create', - 'user123', - { requestBody: { name: 'Test User' } }, - ); - - expect(context).toEqual({ - entityType: 'User', - operation: 'create', - source: 'controllers/user.controller.ts@create', - entityId: 'user123', - additionalInfo: { requestBody: { name: 'Test User' } }, - }); - }); - - it('should build a context object with only required parameters', () => { - const context = buildErrorContext( - 'User', - 'list', - 'controllers/user.controller.ts@list', - ); - - expect(context).toEqual({ - entityType: 'User', - operation: 'list', - source: 'controllers/user.controller.ts@list', - }); - }); - - it('should handle object entityId', () => { - const context = buildErrorContext( - 'Document', - 'get', - 'controllers/document.controller.ts@get', - { project: 'project1', id: 'doc123' }, - ); - - expect(context).toEqual({ - entityType: 'Document', - operation: 'get', - source: 'controllers/document.controller.ts@get', - entityId: { project: 'project1', id: 'doc123' }, - }); - }); - }); -}); diff --git a/src/utils/error-handler.util.ts b/src/utils/error-handler.util.ts deleted file mode 100644 index 51bed29..0000000 --- a/src/utils/error-handler.util.ts +++ /dev/null @@ -1,307 +0,0 @@ -import { createApiError } from './error.util.js'; -import { Logger } from './logger.util.js'; - -/** - * Standard error codes for consistent handling - */ -export enum ErrorCode { - NOT_FOUND = 'NOT_FOUND', - INVALID_CURSOR = 'INVALID_CURSOR', - ACCESS_DENIED = 'ACCESS_DENIED', - VALIDATION_ERROR = 'VALIDATION_ERROR', - UNEXPECTED_ERROR = 'UNEXPECTED_ERROR', - NETWORK_ERROR = 'NETWORK_ERROR', - RATE_LIMIT_ERROR = 'RATE_LIMIT_ERROR', -} - -/** - * Context information for error handling - */ -export interface ErrorContext { - /** - * Source of the error (e.g., file path and function) - */ - source?: string; - - /** - * Type of entity being processed (e.g., 'User') - */ - entityType?: string; - - /** - * Identifier of the entity being processed - */ - entityId?: string | Record; - - /** - * Operation being performed (e.g., 'retrieving', 'searching') - */ - operation?: string; - - /** - * Additional information for debugging - */ - additionalInfo?: Record; -} - -/** - * Helper function to create a consistent error context object - * @param entityType Type of entity being processed - * @param operation Operation being performed - * @param source Source of the error (typically file path and function) - * @param entityId Optional identifier of the entity - * @param additionalInfo Optional additional information for debugging - * @returns A formatted ErrorContext object - */ -export function buildErrorContext( - entityType: string, - operation: string, - source: string, - entityId?: string | Record, - additionalInfo?: Record, -): ErrorContext { - return { - entityType, - operation, - source, - ...(entityId && { entityId }), - ...(additionalInfo && { additionalInfo }), - }; -} - -/** - * Detect specific error types from raw errors - * @param error The error to analyze - * @param context Context information for better error detection - * @returns Object containing the error code and status code - */ -export function detectErrorType( - error: unknown, - context: ErrorContext = {}, -): { code: ErrorCode; statusCode: number } { - const methodLogger = Logger.forContext( - 'utils/error-handler.util.ts', - 'detectErrorType', - ); - methodLogger.debug(`Detecting error type`, { error, context }); - - const errorMessage = error instanceof Error ? error.message : String(error); - const statusCode = - error instanceof Error && 'statusCode' in error - ? (error as { statusCode: number }).statusCode - : undefined; - - // Network error detection - if ( - errorMessage.includes('network error') || - errorMessage.includes('fetch failed') || - errorMessage.includes('ECONNREFUSED') || - errorMessage.includes('ENOTFOUND') || - errorMessage.includes('Failed to fetch') || - errorMessage.includes('Network request failed') - ) { - return { code: ErrorCode.NETWORK_ERROR, statusCode: 500 }; - } - - // Rate limiting detection - if ( - errorMessage.includes('rate limit') || - errorMessage.includes('too many requests') || - statusCode === 429 - ) { - return { code: ErrorCode.RATE_LIMIT_ERROR, statusCode: 429 }; - } - - // Not Found detection - if ( - errorMessage.includes('not found') || - errorMessage.includes('does not exist') || - statusCode === 404 - ) { - return { code: ErrorCode.NOT_FOUND, statusCode: 404 }; - } - - // Access Denied detection - if ( - errorMessage.includes('access') || - errorMessage.includes('permission') || - errorMessage.includes('authorize') || - errorMessage.includes('authentication') || - statusCode === 401 || - statusCode === 403 - ) { - return { code: ErrorCode.ACCESS_DENIED, statusCode: statusCode || 403 }; - } - - // Invalid Cursor detection - if ( - (errorMessage.includes('cursor') || - errorMessage.includes('startAt') || - errorMessage.includes('page')) && - (errorMessage.includes('invalid') || errorMessage.includes('not valid')) - ) { - return { code: ErrorCode.INVALID_CURSOR, statusCode: 400 }; - } - - // Validation Error detection - if ( - errorMessage.includes('validation') || - errorMessage.includes('invalid') || - errorMessage.includes('required') || - statusCode === 400 || - statusCode === 422 - ) { - return { - code: ErrorCode.VALIDATION_ERROR, - statusCode: statusCode || 400, - }; - } - - // Default to unexpected error - return { - code: ErrorCode.UNEXPECTED_ERROR, - statusCode: statusCode || 500, - }; -} - -/** - * Create user-friendly error messages based on error type and context - * @param code The error code - * @param context Context information for better error messages - * @param originalMessage The original error message - * @returns User-friendly error message - */ -export function createUserFriendlyErrorMessage( - code: ErrorCode, - context: ErrorContext = {}, - originalMessage?: string, -): string { - const methodLogger = Logger.forContext( - 'utils/error-handler.util.ts', - 'createUserFriendlyErrorMessage', - ); - const { entityType, entityId, operation } = context; - - // Format entity ID for display - let entityIdStr = ''; - if (entityId) { - if (typeof entityId === 'string') { - entityIdStr = entityId; - } else { - // Handle object entityId - entityIdStr = Object.values(entityId).join('/'); - } - } - - // Determine entity display name - const entity = entityType - ? `${entityType}${entityIdStr ? ` ${entityIdStr}` : ''}` - : 'Resource'; - - let message = ''; - - switch (code) { - case ErrorCode.NOT_FOUND: - message = `${entity} not found${entityIdStr ? `: ${entityIdStr}` : ''}. Verify the ID is correct and that you have access to this ${entityType?.toLowerCase() || 'resource'}.`; - break; - - case ErrorCode.ACCESS_DENIED: - message = `Access denied for ${entity.toLowerCase()}${entityIdStr ? ` ${entityIdStr}` : ''}. Verify your credentials and permissions.`; - break; - - case ErrorCode.INVALID_CURSOR: - message = `Invalid pagination cursor. Use the exact cursor string returned from previous results.`; - break; - - case ErrorCode.VALIDATION_ERROR: - message = - originalMessage || - `Invalid data provided for ${operation || 'operation'} ${entity.toLowerCase()}.`; - break; - - case ErrorCode.NETWORK_ERROR: - message = `Network error while ${operation || 'connecting to'} the service. Please check your internet connection and try again.`; - break; - - case ErrorCode.RATE_LIMIT_ERROR: - message = `Rate limit exceeded. Please wait a moment and try again, or reduce the frequency of requests.`; - break; - - default: - message = `An unexpected error occurred while ${operation || 'processing'} ${entity.toLowerCase()}.`; - } - - // Include original message details if available and appropriate - if ( - originalMessage && - code !== ErrorCode.NOT_FOUND && - code !== ErrorCode.ACCESS_DENIED - ) { - message += ` Error details: ${originalMessage}`; - } - - methodLogger.debug(`Created user-friendly message: ${message}`, { - code, - context, - }); - return message; -} - -/** - * Handle controller errors consistently - * @param error The error to handle - * @param context Context information for better error messages - * @returns Never returns, always throws an error - */ -export function handleControllerError( - error: unknown, - context: ErrorContext = {}, -): never { - const methodLogger = Logger.forContext( - 'utils/error-handler.util.ts', - 'handleControllerError', - ); - - // Extract error details - const errorMessage = error instanceof Error ? error.message : String(error); - const statusCode = - error instanceof Error && 'statusCode' in error - ? (error as { statusCode: number }).statusCode - : undefined; - - // Detect error type using utility - const { code, statusCode: detectedStatus } = detectErrorType( - error, - context, - ); - - // Combine detected status with explicit status - const finalStatusCode = statusCode || detectedStatus; - - // Format entity information for logging - const { entityType, entityId, operation } = context; - const entity = entityType || 'resource'; - const entityIdStr = entityId - ? typeof entityId === 'string' - ? entityId - : JSON.stringify(entityId) - : ''; - const actionStr = operation || 'processing'; - - // Log detailed error information - methodLogger.error( - `Error ${actionStr} ${entity}${ - entityIdStr ? `: ${entityIdStr}` : '' - }: ${errorMessage}`, - error, - ); - - // Create user-friendly error message for the response - const message = - code === ErrorCode.VALIDATION_ERROR - ? errorMessage - : createUserFriendlyErrorMessage(code, context, errorMessage); - - // Throw an appropriate API error with the user-friendly message - throw createApiError(message, finalStatusCode, error); -} diff --git a/src/utils/error.util.test.ts b/src/utils/error.util.test.ts index 86e3635..36b55c3 100644 --- a/src/utils/error.util.test.ts +++ b/src/utils/error.util.test.ts @@ -1,29 +1,35 @@ import { McpError, ErrorType, - createApiError, - createUnexpectedError, getDeepOriginalError, formatErrorForMcpTool, + createUnexpectedError, + ensureMcpError, } from './error.util.js'; describe('error.util', () => { - describe('getDeepOriginalError', () => { - it('should return the deepest original error in a chain', () => { - // Create a nested chain of errors - const deepestError = new Error('Root cause'); - const middleError = createApiError( - 'Middle error', - 500, - deepestError, - ); - const topError = createUnexpectedError('Top error', middleError); + describe('McpError', () => { + it('should create an error with the correct properties', () => { + const error = new McpError('Test error', ErrorType.API_ERROR, 404); - // Should extract the deepest error - const result = getDeepOriginalError(topError); - expect(result).toBe(deepestError); + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(McpError); + expect(error.message).toBe('Test error'); + expect(error.type).toBe(ErrorType.API_ERROR); + expect(error.statusCode).toBe(404); + expect(error.name).toBe('McpError'); }); + }); + describe('createUnexpectedError', () => { + it('should create unexpected error', () => { + const error = createUnexpectedError(); + expect(error).toBeInstanceOf(McpError); + expect(error.type).toBe(ErrorType.UNEXPECTED_ERROR); + expect(error.message).toBe('An unexpected error occurred'); + }); + }); + describe('getDeepOriginalError', () => { it('should handle null/undefined input', () => { expect(getDeepOriginalError(null)).toBeNull(); expect(getDeepOriginalError(undefined)).toBeUndefined(); @@ -57,30 +63,37 @@ describe('error.util', () => { }); }); - describe('formatErrorForMcpTool', () => { - it('should format McpError with metadata', () => { - const error = createApiError('Test error', 404, { - detail: 'Not found', - }); - const result = formatErrorForMcpTool(error); + describe('ensureMcpError', () => { + it('should wrap a standard Error', () => { + const originalError = new Error('Standard error'); + const error = ensureMcpError(originalError); - // Check the content - expect(result.content).toEqual([ - { - type: 'text', - text: 'Error: Test error', - }, - ]); - - // Check the metadata - expect(result.metadata).toBeDefined(); - expect(result.metadata?.errorType).toBe(ErrorType.API_ERROR); - expect(result.metadata?.statusCode).toBe(404); - expect(result.metadata?.errorDetails).toEqual({ - detail: 'Not found', - }); + expect(error).toBeInstanceOf(McpError); + expect(error.type).toBe(ErrorType.UNEXPECTED_ERROR); + expect(error.message).toBe('Standard error'); + expect(error.originalError).toBe(originalError); }); + it('should handle non-Error objects', () => { + const error = ensureMcpError('String error'); + + expect(error).toBeInstanceOf(McpError); + expect(error.type).toBe(ErrorType.UNEXPECTED_ERROR); + expect(error.message).toBe('String error'); + }); + + it('should return the same McpError instance if passed in', () => { + const mcpError = new McpError( + 'Already MCP', + ErrorType.API_ERROR, + 400, + ); + const result = ensureMcpError(mcpError); + expect(result).toBe(mcpError); + }); + }); + + describe('formatErrorForMcpTool', () => { it('should wrap non-McpError with metadata', () => { const error = new Error('Regular error'); const result = formatErrorForMcpTool(error); @@ -101,15 +114,5 @@ describe('error.util', () => { expect(result.content[0].text).toBe('Error: String error'); expect(result.metadata?.errorType).toBe(ErrorType.UNEXPECTED_ERROR); }); - - it('should extract deep original error details', () => { - const deepError = { code: 'DEEP_ERROR', message: 'Deep cause' }; - const middleError = createApiError('Middle layer', 500, deepError); - const topError = createUnexpectedError('Top error', middleError); - - const result = formatErrorForMcpTool(topError); - - expect(result.metadata?.errorDetails).toEqual(deepError); - }); }); }); diff --git a/src/utils/error.util.ts b/src/utils/error.util.ts index 1652106..ecb9f60 100644 --- a/src/utils/error.util.ts +++ b/src/utils/error.util.ts @@ -32,40 +32,6 @@ export class McpError extends Error { } } -/** - * Create an authentication missing error - */ -export function createAuthMissingError( - message: string = 'Authentication credentials are missing', -): McpError { - return new McpError(message, ErrorType.AUTH_MISSING); -} - -/** - * Create an authentication invalid error - */ -export function createAuthInvalidError( - message: string = 'Authentication credentials are invalid', -): McpError { - return new McpError(message, ErrorType.AUTH_INVALID, 401); -} - -/** - * Create an API error - */ -export function createApiError( - message: string, - statusCode?: number, - originalError?: unknown, -): McpError { - return new McpError( - message, - ErrorType.API_ERROR, - statusCode, - originalError, - ); -} - /** * Create an unexpected error */ @@ -164,77 +130,3 @@ export function formatErrorForMcpTool(error: unknown): { }, }; } - -/** - * Format error for MCP resource response - */ -export function formatErrorForMcpResource( - error: unknown, - uri: string, -): { - contents: Array<{ - uri: string; - text: string; - mimeType: string; - description?: string; - }>; -} { - const methodLogger = Logger.forContext( - 'utils/error.util.ts', - 'formatErrorForMcpResource', - ); - const mcpError = ensureMcpError(error); - methodLogger.error(`${mcpError.type} error`, mcpError); - - return { - contents: [ - { - uri, - text: `Error: ${mcpError.message}`, - mimeType: 'text/plain', - description: `Error: ${mcpError.type}`, - }, - ], - }; -} - -/** - * Handle error in CLI context with improved user feedback - */ -export function handleCliError(error: unknown): never { - const methodLogger = Logger.forContext( - 'utils/error.util.ts', - 'handleCliError', - ); - const mcpError = ensureMcpError(error); - methodLogger.error(`${mcpError.type} error`, mcpError); - - // Print the error message - console.error(`Error: ${mcpError.message}`); - - // Provide helpful context based on error type - if (mcpError.type === ErrorType.AUTH_MISSING) { - console.error( - '\nTip: Make sure to set up your API token in the configuration file or environment variables.', - ); - } else if (mcpError.type === ErrorType.AUTH_INVALID) { - console.error( - '\nTip: Check that your API token is correct and has not expired.', - ); - } else if (mcpError.type === ErrorType.API_ERROR) { - if (mcpError.statusCode === 429) { - console.error( - '\nTip: You may have exceeded your API rate limits. Try again later or upgrade your API plan.', - ); - } - } - - // Display DEBUG tip - if (process.env.DEBUG !== 'mcp:*') { - console.error( - '\nFor more detailed error information, run with DEBUG=mcp:* environment variable.', - ); - } - - process.exit(1); -} diff --git a/src/utils/formatter.util.ts b/src/utils/formatter.util.ts deleted file mode 100644 index 1d8147b..0000000 --- a/src/utils/formatter.util.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Standardized formatting utilities for consistent output across all CLI and Tool interfaces. - * These functions should be used by all formatters to ensure consistent formatting. - */ - -/** - * Format a date in a standardized way: YYYY-MM-DD HH:MM:SS UTC - * @param dateString - ISO date string or Date object - * @returns Formatted date string - */ -export function formatDate(dateString?: string | Date): string { - if (!dateString) { - return 'Not available'; - } - - try { - const date = - typeof dateString === 'string' ? new Date(dateString) : dateString; - - // Format: YYYY-MM-DD HH:MM:SS UTC - return date - .toISOString() - .replace('T', ' ') - .replace(/\.\d+Z$/, ' UTC'); - } catch { - return 'Invalid date'; - } -} - -/** - * Format a URL as a markdown link - * @param url - URL to format - * @param title - Link title - * @returns Formatted markdown link - */ -export function formatUrl(url?: string, title?: string): string { - if (!url) { - return 'Not available'; - } - - const linkTitle = title || url; - return `[${linkTitle}](${url})`; -} - -/** - * Format a heading with consistent style - * @param text - Heading text - * @param level - Heading level (1-6) - * @returns Formatted heading - */ -export function formatHeading(text: string, level: number = 1): string { - const validLevel = Math.min(Math.max(level, 1), 6); - const prefix = '#'.repeat(validLevel); - return `${prefix} ${text}`; -} - -/** - * Format a list of key-value pairs as a bullet list - * @param items - Object with key-value pairs - * @param keyFormatter - Optional function to format keys - * @returns Formatted bullet list - */ -export function formatBulletList( - items: Record, - keyFormatter?: (key: string) => string, -): string { - const lines: string[] = []; - - for (const [key, value] of Object.entries(items)) { - if (value === undefined || value === null) { - continue; - } - - const formattedKey = keyFormatter ? keyFormatter(key) : key; - const formattedValue = formatValue(value); - lines.push(`- **${formattedKey}**: ${formattedValue}`); - } - - return lines.join('\n'); -} - -/** - * Format a value based on its type - * @param value - Value to format - * @returns Formatted value - */ -function formatValue(value: unknown): string { - if (value === undefined || value === null) { - return 'Not available'; - } - - if (value instanceof Date) { - return formatDate(value); - } - - // Handle URL objects with url and title properties - if (typeof value === 'object' && value !== null && 'url' in value) { - const urlObj = value as { url: string; title?: string }; - if (typeof urlObj.url === 'string') { - return formatUrl(urlObj.url, urlObj.title); - } - } - - if (typeof value === 'string') { - // Check if it's a URL - if (value.startsWith('http://') || value.startsWith('https://')) { - return formatUrl(value); - } - - // Check if it might be a date - if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) { - return formatDate(value); - } - - return value; - } - - if (typeof value === 'boolean') { - return value ? 'Yes' : 'No'; - } - - return String(value); -} - -/** - * Format a separator line - * @returns Separator line - */ -export function formatSeparator(): string { - return '---'; -} diff --git a/src/utils/logger.util.ts b/src/utils/logger.util.ts index d082755..851aa89 100644 --- a/src/utils/logger.util.ts +++ b/src/utils/logger.util.ts @@ -194,7 +194,7 @@ let isLoggerInitialized = false; * 5. Set DEBUG environment variable to control which modules show debug logs: * - DEBUG=true (enable all debug logs) * - DEBUG=controllers/*,services/* (enable for specific module groups) - * - DEBUG=transport,utils/formatter* (enable specific modules, supports wildcards) + * - DEBUG=transport,utils/error* (enable specific modules, supports wildcards) */ class Logger { private context?: string; diff --git a/src/utils/transport.util.ts b/src/utils/transport.util.ts deleted file mode 100644 index 049ed59..0000000 --- a/src/utils/transport.util.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Logger } from './logger.util.js'; -import { - createApiError, - createAuthInvalidError, - createUnexpectedError, - McpError, -} from './error.util.js'; - -// Create a contextualized logger for this file -const transportLogger = Logger.forContext('utils/transport.util.ts'); - -// Log transport utility initialization -transportLogger.debug('Transport utility initialized'); - -/** - * Interface for HTTP request options - */ -export interface RequestOptions { - method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; - headers?: Record; - body?: unknown; -} - -/** - * Generic and reusable function to fetch data from any API endpoint. - * Handles standard HTTP request setup, response checking, basic error handling, and logging. - * - * @param url The full URL to fetch data from. - * @param options Request options including method, headers, and body. - * @returns The response data parsed as type T. - * @throws {McpError} If the request fails, including network errors, non-OK HTTP status, or JSON parsing issues. - */ -export async function fetchApi( - url: string, - options: RequestOptions = {}, -): Promise { - const methodLogger = Logger.forContext( - 'utils/transport.util.ts', - 'fetchApi', - ); - - // Prepare standard request options - const requestOptions: RequestInit = { - method: options.method || 'GET', - headers: { - // Standard headers, allow overrides via options.headers - 'Content-Type': 'application/json', - Accept: 'application/json', - ...options.headers, - }, - body: options.body ? JSON.stringify(options.body) : undefined, - }; - - methodLogger.debug(`Executing API call: ${requestOptions.method} ${url}`); - const startTime = performance.now(); // Track performance - - try { - const response = await fetch(url, requestOptions); - const endTime = performance.now(); - const duration = (endTime - startTime).toFixed(2); - - methodLogger.debug( - `API call completed in ${duration}ms with status: ${response.status} ${response.statusText}`, - { url, status: response.status }, - ); - - // Check if the response status is OK (2xx) - if (!response.ok) { - const errorText = await response.text(); // Get error body for context - methodLogger.error( - `API error response (${response.status}):`, - errorText, - ); - - // Classify standard HTTP errors - if (response.status === 401) { - throw createAuthInvalidError( - 'Authentication failed. Check API token if required.', - ); - } else if (response.status === 403) { - throw createAuthInvalidError( - 'Permission denied for the requested resource.', - ); - } else if (response.status === 404) { - throw createApiError( - 'Resource not found at the specified URL.', - response.status, - errorText, - ); - } else { - // Generic API error for other non-2xx statuses - throw createApiError( - `API request failed with status ${response.status}: ${response.statusText}`, - response.status, - errorText, - ); - } - } - - // Attempt to parse the response body as JSON - try { - const responseData = await response.json(); - methodLogger.debug('Response body successfully parsed as JSON.'); - // methodLogger.debug('Response Data:', responseData); // Uncomment for full response logging - return responseData as T; - } catch (parseError) { - methodLogger.error( - 'Failed to parse API response JSON:', - parseError, - ); - // Throw a specific error for JSON parsing failure - throw createApiError( - `Failed to parse API response JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`, - response.status, // Include original status for context - parseError, - ); - } - } catch (error) { - const endTime = performance.now(); - const duration = (endTime - startTime).toFixed(2); - methodLogger.error( - `API call failed after ${duration}ms for ${url}:`, - error, - ); - - // Rethrow if it's already an McpError (e.g., from status checks or parsing) - if (error instanceof McpError) { - throw error; - } - - // Handle potential network errors (TypeError in fetch) - if (error instanceof TypeError) { - throw createApiError( - `Network error during API call: ${error.message}`, - undefined, // No specific HTTP status for network errors - error, - ); - } - - // Wrap any other unexpected errors - throw createUnexpectedError('Unexpected error during API call', error); - } -}