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:
2026-02-06 14:59:29 -03:00
parent 8b6e59d346
commit 771cf6cf50
11 changed files with 1053 additions and 0 deletions

View 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
View 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`);
});

View 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"
}
}

View 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));
});