feat: Add OpenTelemetry OTLP HTTP receiver
- Add POST /v1/traces endpoint for OTLP JSON trace ingestion - Convert OTLP spans to internal format and save to PostgreSQL - Manual JSON parsing (no Go 1.24 dependencies) - Add Node.js instrumentation example with Express - Add Python instrumentation example with Flask - Auto-instrumentation support for both languages
This commit is contained in:
62
examples/otel-nodejs/README.md
Normal file
62
examples/otel-nodejs/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Node.js OpenTelemetry Example for Ophion
|
||||
|
||||
This example demonstrates how to instrument a Node.js application with OpenTelemetry and send traces to Ophion.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start Ophion server (in another terminal)
|
||||
# cd ~/projetos_jarvis/ophion && go run cmd/server/main.go
|
||||
|
||||
# Run the app with tracing
|
||||
npm run trace
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:8080/v1/traces` | Ophion OTLP endpoint |
|
||||
| `OTEL_SERVICE_NAME` | `nodejs-example` | Service name in traces |
|
||||
| `PORT` | `3000` | App HTTP port |
|
||||
|
||||
## Test Endpoints
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
curl http://localhost:3000/health
|
||||
|
||||
# Get all users (generates trace)
|
||||
curl http://localhost:3000/users
|
||||
|
||||
# Get single user
|
||||
curl http://localhost:3000/users/1
|
||||
|
||||
# Create order (complex trace with nested spans)
|
||||
curl -X POST http://localhost:3000/orders \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"items": [{"id": 1, "qty": 2}]}'
|
||||
|
||||
# Trigger error (error trace)
|
||||
curl http://localhost:3000/error
|
||||
```
|
||||
|
||||
## View Traces in Ophion
|
||||
|
||||
```bash
|
||||
# List recent traces
|
||||
curl http://localhost:8080/api/v1/traces
|
||||
|
||||
# Get specific trace
|
||||
curl http://localhost:8080/api/v1/traces/<trace_id>
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. `tracing.js` - Initializes OpenTelemetry SDK with OTLP HTTP exporter
|
||||
2. Auto-instrumentation captures HTTP requests automatically
|
||||
3. Manual spans in `app.js` add custom business logic traces
|
||||
4. All spans are sent to Ophion's `/v1/traces` endpoint in OTLP JSON format
|
||||
160
examples/otel-nodejs/app.js
Normal file
160
examples/otel-nodejs/app.js
Normal file
@@ -0,0 +1,160 @@
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 📱 Example Express App with OpenTelemetry
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
//
|
||||
// Run with: npm run trace
|
||||
// Or: node --require ./tracing.js app.js
|
||||
|
||||
const express = require('express');
|
||||
const { trace, SpanStatusCode } = require('@opentelemetry/api');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Get a tracer for manual instrumentation
|
||||
const tracer = trace.getTracer('example-app');
|
||||
|
||||
// Simulated database
|
||||
const users = [
|
||||
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
||||
{ id: 2, name: 'Bob', email: 'bob@example.com' },
|
||||
{ id: 3, name: 'Charlie', email: 'charlie@example.com' },
|
||||
];
|
||||
|
||||
// Middleware to add request tracing
|
||||
app.use((req, res, next) => {
|
||||
const span = trace.getActiveSpan();
|
||||
if (span) {
|
||||
span.setAttribute('http.user_agent', req.get('user-agent') || 'unknown');
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'healthy', service: 'nodejs-example' });
|
||||
});
|
||||
|
||||
// Get all users (with manual span)
|
||||
app.get('/users', async (req, res) => {
|
||||
// Create a custom span for database operation
|
||||
const span = tracer.startSpan('db.query.users');
|
||||
span.setAttribute('db.system', 'memory');
|
||||
span.setAttribute('db.operation', 'SELECT');
|
||||
|
||||
try {
|
||||
// Simulate database latency
|
||||
await sleep(Math.random() * 100);
|
||||
|
||||
span.setAttribute('db.row_count', users.length);
|
||||
span.setStatus({ code: SpanStatusCode.OK });
|
||||
|
||||
res.json({ users });
|
||||
} catch (error) {
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: error.message,
|
||||
});
|
||||
span.recordException(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
});
|
||||
|
||||
// Get user by ID
|
||||
app.get('/users/:id', async (req, res) => {
|
||||
const userId = parseInt(req.params.id);
|
||||
|
||||
const span = tracer.startSpan('db.query.user_by_id');
|
||||
span.setAttribute('db.system', 'memory');
|
||||
span.setAttribute('db.operation', 'SELECT');
|
||||
span.setAttribute('user.id', userId);
|
||||
|
||||
try {
|
||||
await sleep(Math.random() * 50);
|
||||
|
||||
const user = users.find(u => u.id === userId);
|
||||
|
||||
if (!user) {
|
||||
span.setStatus({ code: SpanStatusCode.ERROR, message: 'User not found' });
|
||||
res.status(404).json({ error: 'User not found' });
|
||||
} else {
|
||||
span.setStatus({ code: SpanStatusCode.OK });
|
||||
res.json({ user });
|
||||
}
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
});
|
||||
|
||||
// Create order (simulates complex operation with nested spans)
|
||||
app.post('/orders', express.json(), async (req, res) => {
|
||||
const parentSpan = tracer.startSpan('order.create');
|
||||
|
||||
try {
|
||||
// Step 1: Validate inventory
|
||||
const inventorySpan = tracer.startSpan('inventory.check', {
|
||||
attributes: { 'order.items': req.body.items?.length || 0 },
|
||||
});
|
||||
await sleep(Math.random() * 100);
|
||||
inventorySpan.setStatus({ code: SpanStatusCode.OK });
|
||||
inventorySpan.end();
|
||||
|
||||
// Step 2: Process payment
|
||||
const paymentSpan = tracer.startSpan('payment.process', {
|
||||
attributes: { 'payment.method': 'credit_card' },
|
||||
});
|
||||
await sleep(Math.random() * 200);
|
||||
paymentSpan.setStatus({ code: SpanStatusCode.OK });
|
||||
paymentSpan.end();
|
||||
|
||||
// Step 3: Create order record
|
||||
const createSpan = tracer.startSpan('db.insert.order');
|
||||
await sleep(Math.random() * 50);
|
||||
const orderId = Date.now().toString(36);
|
||||
createSpan.setAttribute('order.id', orderId);
|
||||
createSpan.setStatus({ code: SpanStatusCode.OK });
|
||||
createSpan.end();
|
||||
|
||||
parentSpan.setStatus({ code: SpanStatusCode.OK });
|
||||
res.json({ orderId, status: 'created' });
|
||||
|
||||
} catch (error) {
|
||||
parentSpan.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
||||
parentSpan.recordException(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
} finally {
|
||||
parentSpan.end();
|
||||
}
|
||||
});
|
||||
|
||||
// Simulate error endpoint (for testing error traces)
|
||||
app.get('/error', (req, res) => {
|
||||
const span = trace.getActiveSpan();
|
||||
const error = new Error('Simulated error for testing');
|
||||
|
||||
if (span) {
|
||||
span.recordException(error);
|
||||
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
||||
}
|
||||
|
||||
res.status(500).json({ error: error.message });
|
||||
});
|
||||
|
||||
// Helper
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Example app listening on http://localhost:${PORT}`);
|
||||
console.log('');
|
||||
console.log('Try these endpoints:');
|
||||
console.log(` GET http://localhost:${PORT}/health`);
|
||||
console.log(` GET http://localhost:${PORT}/users`);
|
||||
console.log(` GET http://localhost:${PORT}/users/1`);
|
||||
console.log(` POST http://localhost:${PORT}/orders`);
|
||||
console.log(` GET http://localhost:${PORT}/error`);
|
||||
});
|
||||
20
examples/otel-nodejs/package.json
Normal file
20
examples/otel-nodejs/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "ophion-otel-nodejs-example",
|
||||
"version": "1.0.0",
|
||||
"description": "Example Node.js app instrumented with OpenTelemetry for Ophion",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"trace": "node --require ./tracing.js app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.7.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.41.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.47.0",
|
||||
"@opentelemetry/resources": "^1.21.0",
|
||||
"@opentelemetry/sdk-node": "^0.47.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.21.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.21.0",
|
||||
"express": "^4.18.2"
|
||||
}
|
||||
}
|
||||
52
examples/otel-nodejs/tracing.js
Normal file
52
examples/otel-nodejs/tracing.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 🔭 OpenTelemetry Tracing Setup for Ophion
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
//
|
||||
// This file initializes OpenTelemetry tracing and sends spans
|
||||
// to Ophion's OTLP HTTP endpoint.
|
||||
//
|
||||
// Usage: node --require ./tracing.js app.js
|
||||
|
||||
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
||||
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
||||
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
||||
const { Resource } = require('@opentelemetry/resources');
|
||||
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
||||
|
||||
// Configure the OTLP exporter to send to Ophion
|
||||
const traceExporter = new OTLPTraceExporter({
|
||||
// Ophion OTLP endpoint - adjust host/port as needed
|
||||
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:8080/v1/traces',
|
||||
headers: {
|
||||
// Optional: add authorization if Ophion requires it
|
||||
// 'Authorization': `Bearer ${process.env.OPHION_API_KEY}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Create the SDK with auto-instrumentation
|
||||
const sdk = new NodeSDK({
|
||||
resource: new Resource({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'nodejs-example',
|
||||
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
|
||||
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
|
||||
}),
|
||||
traceExporter,
|
||||
instrumentations: [
|
||||
getNodeAutoInstrumentations({
|
||||
// Disable fs instrumentation to reduce noise
|
||||
'@opentelemetry/instrumentation-fs': { enabled: false },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Start the SDK
|
||||
sdk.start();
|
||||
console.log('🔭 OpenTelemetry tracing initialized - sending to Ophion');
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
sdk.shutdown()
|
||||
.then(() => console.log('OpenTelemetry SDK shut down'))
|
||||
.catch((error) => console.error('Error shutting down SDK', error))
|
||||
.finally(() => process.exit(0));
|
||||
});
|
||||
85
examples/otel-python/README.md
Normal file
85
examples/otel-python/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Python OpenTelemetry Example for Ophion
|
||||
|
||||
This example demonstrates how to instrument a Python Flask application with OpenTelemetry and send traces to Ophion.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Create virtual environment (recommended)
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# or: venv\Scripts\activate # Windows
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Start Ophion server (in another terminal)
|
||||
# cd ~/projetos_jarvis/ophion && go run cmd/server/main.go
|
||||
|
||||
# Run the app
|
||||
python app.py
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:8080` | Ophion base URL |
|
||||
| `OTEL_SERVICE_NAME` | `python-example` | Service name in traces |
|
||||
| `PORT` | `5000` | App HTTP port |
|
||||
|
||||
## Test Endpoints
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
curl http://localhost:5000/health
|
||||
|
||||
# Get all users (generates trace)
|
||||
curl http://localhost:5000/users
|
||||
|
||||
# Get single user
|
||||
curl http://localhost:5000/users/1
|
||||
|
||||
# Create order (complex trace with nested spans)
|
||||
curl -X POST http://localhost:5000/orders \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"items": [{"id": 1, "qty": 2}]}'
|
||||
|
||||
# Trigger error (error trace)
|
||||
curl http://localhost:5000/error
|
||||
|
||||
# External HTTP call (distributed tracing)
|
||||
curl http://localhost:5000/external-call
|
||||
```
|
||||
|
||||
## View Traces in Ophion
|
||||
|
||||
```bash
|
||||
# List recent traces
|
||||
curl http://localhost:8080/api/v1/traces
|
||||
|
||||
# Get specific trace
|
||||
curl http://localhost:8080/api/v1/traces/<trace_id>
|
||||
```
|
||||
|
||||
## Auto-Instrumentation Alternative
|
||||
|
||||
You can also use OpenTelemetry's auto-instrumentation:
|
||||
|
||||
```bash
|
||||
# Install auto-instrumentation
|
||||
pip install opentelemetry-distro
|
||||
opentelemetry-bootstrap -a install
|
||||
|
||||
# Run with auto-instrumentation
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:8080 \
|
||||
OTEL_SERVICE_NAME=python-example \
|
||||
opentelemetry-instrument python app.py
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. `tracing.py` - Initializes OpenTelemetry SDK with OTLP HTTP exporter
|
||||
2. `FlaskInstrumentor` auto-captures HTTP requests
|
||||
3. Manual spans in `app.py` add custom business logic traces
|
||||
4. All spans are sent to Ophion's `/v1/traces` endpoint in OTLP proto/JSON format
|
||||
168
examples/otel-python/app.py
Normal file
168
examples/otel-python/app.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
═══════════════════════════════════════════════════════════
|
||||
📱 Example Flask App with OpenTelemetry
|
||||
═══════════════════════════════════════════════════════════
|
||||
|
||||
Run with:
|
||||
python app.py
|
||||
|
||||
Or with auto-instrumentation:
|
||||
opentelemetry-instrument python app.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
import atexit
|
||||
from flask import Flask, jsonify, request
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.trace import Status, StatusCode
|
||||
from opentelemetry.instrumentation.flask import FlaskInstrumentor
|
||||
|
||||
# Initialize tracing BEFORE creating Flask app
|
||||
from tracing import init_tracing, get_tracer, shutdown
|
||||
|
||||
init_tracing()
|
||||
tracer = get_tracer()
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Auto-instrument Flask
|
||||
FlaskInstrumentor().instrument_app(app)
|
||||
|
||||
# Register shutdown handler
|
||||
atexit.register(shutdown)
|
||||
|
||||
# Simulated database
|
||||
users = [
|
||||
{"id": 1, "name": "Alice", "email": "alice@example.com"},
|
||||
{"id": 2, "name": "Bob", "email": "bob@example.com"},
|
||||
{"id": 3, "name": "Charlie", "email": "charlie@example.com"},
|
||||
]
|
||||
|
||||
|
||||
@app.route('/health')
|
||||
def health():
|
||||
"""Health check endpoint."""
|
||||
return jsonify({"status": "healthy", "service": "python-example"})
|
||||
|
||||
|
||||
@app.route('/users')
|
||||
def get_users():
|
||||
"""Get all users with custom span."""
|
||||
with tracer.start_as_current_span("db.query.users") as span:
|
||||
span.set_attribute("db.system", "memory")
|
||||
span.set_attribute("db.operation", "SELECT")
|
||||
|
||||
# Simulate database latency
|
||||
time.sleep(random.uniform(0.01, 0.1))
|
||||
|
||||
span.set_attribute("db.row_count", len(users))
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
|
||||
return jsonify({"users": users})
|
||||
|
||||
|
||||
@app.route('/users/<int:user_id>')
|
||||
def get_user(user_id: int):
|
||||
"""Get user by ID."""
|
||||
with tracer.start_as_current_span("db.query.user_by_id") as span:
|
||||
span.set_attribute("db.system", "memory")
|
||||
span.set_attribute("db.operation", "SELECT")
|
||||
span.set_attribute("user.id", user_id)
|
||||
|
||||
# Simulate database latency
|
||||
time.sleep(random.uniform(0.01, 0.05))
|
||||
|
||||
user = next((u for u in users if u["id"] == user_id), None)
|
||||
|
||||
if not user:
|
||||
span.set_status(Status(StatusCode.ERROR, "User not found"))
|
||||
return jsonify({"error": "User not found"}), 404
|
||||
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
return jsonify({"user": user})
|
||||
|
||||
|
||||
@app.route('/orders', methods=['POST'])
|
||||
def create_order():
|
||||
"""Create order with nested spans."""
|
||||
with tracer.start_as_current_span("order.create") as parent_span:
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
items = data.get("items", [])
|
||||
|
||||
# Step 1: Validate inventory
|
||||
with tracer.start_as_current_span("inventory.check") as span:
|
||||
span.set_attribute("order.items", len(items))
|
||||
time.sleep(random.uniform(0.05, 0.1))
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
|
||||
# Step 2: Process payment
|
||||
with tracer.start_as_current_span("payment.process") as span:
|
||||
span.set_attribute("payment.method", "credit_card")
|
||||
time.sleep(random.uniform(0.1, 0.2))
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
|
||||
# Step 3: Create order record
|
||||
with tracer.start_as_current_span("db.insert.order") as span:
|
||||
time.sleep(random.uniform(0.02, 0.05))
|
||||
order_id = hex(int(time.time() * 1000))[2:]
|
||||
span.set_attribute("order.id", order_id)
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
|
||||
parent_span.set_status(Status(StatusCode.OK))
|
||||
return jsonify({"orderId": order_id, "status": "created"})
|
||||
|
||||
except Exception as e:
|
||||
parent_span.set_status(Status(StatusCode.ERROR, str(e)))
|
||||
parent_span.record_exception(e)
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/error')
|
||||
def trigger_error():
|
||||
"""Trigger error for testing."""
|
||||
span = trace.get_current_span()
|
||||
error = Exception("Simulated error for testing")
|
||||
|
||||
span.record_exception(error)
|
||||
span.set_status(Status(StatusCode.ERROR, str(error)))
|
||||
|
||||
return jsonify({"error": str(error)}), 500
|
||||
|
||||
|
||||
@app.route('/external-call')
|
||||
def external_call():
|
||||
"""Make external HTTP call (demonstrates distributed tracing)."""
|
||||
import requests
|
||||
|
||||
with tracer.start_as_current_span("external.http.call") as span:
|
||||
span.set_attribute("http.url", "https://httpbin.org/get")
|
||||
|
||||
try:
|
||||
# Note: requests auto-instrumentation propagates trace context
|
||||
response = requests.get("https://httpbin.org/get", timeout=5)
|
||||
span.set_attribute("http.status_code", response.status_code)
|
||||
span.set_status(Status(StatusCode.OK))
|
||||
return jsonify({"status": "ok", "external_status": response.status_code})
|
||||
except Exception as e:
|
||||
span.record_exception(e)
|
||||
span.set_status(Status(StatusCode.ERROR, str(e)))
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = int(os.getenv('PORT', 5000))
|
||||
print(f"""
|
||||
🚀 Example app listening on http://localhost:{port}
|
||||
|
||||
Try these endpoints:
|
||||
GET http://localhost:{port}/health
|
||||
GET http://localhost:{port}/users
|
||||
GET http://localhost:{port}/users/1
|
||||
POST http://localhost:{port}/orders
|
||||
GET http://localhost:{port}/error
|
||||
GET http://localhost:{port}/external-call
|
||||
""")
|
||||
app.run(host='0.0.0.0', port=port, debug=False)
|
||||
15
examples/otel-python/requirements.txt
Normal file
15
examples/otel-python/requirements.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
# OpenTelemetry SDK and API
|
||||
opentelemetry-api>=1.22.0
|
||||
opentelemetry-sdk>=1.22.0
|
||||
|
||||
# OTLP HTTP Exporter
|
||||
opentelemetry-exporter-otlp-proto-http>=1.22.0
|
||||
|
||||
# Auto-instrumentation
|
||||
opentelemetry-instrumentation>=0.43b0
|
||||
opentelemetry-instrumentation-flask>=0.43b0
|
||||
opentelemetry-instrumentation-requests>=0.43b0
|
||||
|
||||
# Web framework
|
||||
flask>=3.0.0
|
||||
requests>=2.31.0
|
||||
84
examples/otel-python/tracing.py
Normal file
84
examples/otel-python/tracing.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
═══════════════════════════════════════════════════════════
|
||||
🔭 OpenTelemetry Tracing Setup for Ophion
|
||||
═══════════════════════════════════════════════════════════
|
||||
|
||||
This module initializes OpenTelemetry tracing and sends spans
|
||||
to Ophion's OTLP HTTP endpoint.
|
||||
|
||||
Usage:
|
||||
from tracing import init_tracing, get_tracer
|
||||
init_tracing()
|
||||
tracer = get_tracer()
|
||||
"""
|
||||
|
||||
import os
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION, DEPLOYMENT_ENVIRONMENT
|
||||
|
||||
# Global tracer
|
||||
_tracer = None
|
||||
|
||||
|
||||
def init_tracing(service_name: str = None) -> None:
|
||||
"""
|
||||
Initialize OpenTelemetry tracing with OTLP HTTP exporter.
|
||||
|
||||
Args:
|
||||
service_name: Name of the service (default: from env or 'python-example')
|
||||
"""
|
||||
global _tracer
|
||||
|
||||
# Get configuration from environment
|
||||
otlp_endpoint = os.getenv('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:8080')
|
||||
service = service_name or os.getenv('OTEL_SERVICE_NAME', 'python-example')
|
||||
environment = os.getenv('DEPLOYMENT_ENVIRONMENT', 'development')
|
||||
|
||||
# Create resource with service info
|
||||
resource = Resource.create({
|
||||
SERVICE_NAME: service,
|
||||
SERVICE_VERSION: '1.0.0',
|
||||
DEPLOYMENT_ENVIRONMENT: environment,
|
||||
})
|
||||
|
||||
# Create tracer provider
|
||||
provider = TracerProvider(resource=resource)
|
||||
|
||||
# Configure OTLP HTTP exporter
|
||||
# Note: endpoint should be base URL, SDK adds /v1/traces
|
||||
exporter = OTLPSpanExporter(
|
||||
endpoint=f"{otlp_endpoint}/v1/traces",
|
||||
# headers={"Authorization": f"Bearer {os.getenv('OPHION_API_KEY', '')}"},
|
||||
)
|
||||
|
||||
# Add batch processor for efficient sending
|
||||
processor = BatchSpanProcessor(exporter)
|
||||
provider.add_span_processor(processor)
|
||||
|
||||
# Set as global tracer provider
|
||||
trace.set_tracer_provider(provider)
|
||||
|
||||
# Create tracer
|
||||
_tracer = trace.get_tracer(__name__)
|
||||
|
||||
print(f"🔭 OpenTelemetry tracing initialized")
|
||||
print(f" Service: {service}")
|
||||
print(f" Endpoint: {otlp_endpoint}/v1/traces")
|
||||
|
||||
|
||||
def get_tracer():
|
||||
"""Get the initialized tracer."""
|
||||
global _tracer
|
||||
if _tracer is None:
|
||||
init_tracing()
|
||||
return _tracer
|
||||
|
||||
|
||||
def shutdown():
|
||||
"""Gracefully shutdown the tracer provider."""
|
||||
provider = trace.get_tracer_provider()
|
||||
if hasattr(provider, 'shutdown'):
|
||||
provider.shutdown()
|
||||
Reference in New Issue
Block a user