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));
|
||||
});
|
||||
Reference in New Issue
Block a user