- 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
169 lines
5.7 KiB
Python
169 lines
5.7 KiB
Python
"""
|
|
═══════════════════════════════════════════════════════════
|
|
📱 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)
|