betelgeusebytes/k8s/observability-stack/demo-app.yaml

254 lines
7.4 KiB
YAML

---
# Example instrumented application to test the observability stack
# This is a simple Python Flask app with OpenTelemetry instrumentation
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-app
namespace: observability
data:
app.py: |
from flask import Flask, jsonify
import logging
import json
import time
import random
# OpenTelemetry imports
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.sdk.resources import Resource
from prometheus_flask_exporter import PrometheusMetrics
# Configure structured logging
logging.basicConfig(
level=logging.INFO,
format='%(message)s'
)
class JSONFormatter(logging.Formatter):
def format(self, record):
log_obj = {
'timestamp': self.formatTime(record, self.datefmt),
'level': record.levelname,
'message': record.getMessage(),
'logger': record.name,
}
if hasattr(record, 'trace_id'):
log_obj['trace_id'] = record.trace_id
log_obj['span_id'] = record.span_id
return json.dumps(log_obj)
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger = logging.getLogger(__name__)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Configure OpenTelemetry
resource = Resource.create({"service.name": "demo-app"})
# Tracing
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint="http://tempo.observability.svc.cluster.local:4317",
insecure=True
)
)
)
trace.set_tracer_provider(trace_provider)
tracer = trace.get_tracer(__name__)
# Create Flask app
app = Flask(__name__)
# Prometheus metrics
metrics = PrometheusMetrics(app)
# Auto-instrument Flask
FlaskInstrumentor().instrument_app(app)
# Sample data
ITEMS = ["apple", "banana", "orange", "grape", "mango"]
@app.route('/')
def index():
span = trace.get_current_span()
trace_id = format(span.get_span_context().trace_id, '032x')
logger.info("Index page accessed", extra={
'trace_id': trace_id,
'endpoint': '/'
})
return jsonify({
'service': 'demo-app',
'status': 'healthy',
'trace_id': trace_id
})
@app.route('/items')
def get_items():
with tracer.start_as_current_span("fetch_items") as span:
# Simulate database query
time.sleep(random.uniform(0.01, 0.1))
span.set_attribute("items.count", len(ITEMS))
trace_id = format(span.get_span_context().trace_id, '032x')
logger.info("Items fetched", extra={
'trace_id': trace_id,
'count': len(ITEMS)
})
return jsonify({
'items': ITEMS,
'count': len(ITEMS),
'trace_id': trace_id
})
@app.route('/item/<int:item_id>')
def get_item(item_id):
with tracer.start_as_current_span("fetch_item") as span:
span.set_attribute("item.id", item_id)
trace_id = format(span.get_span_context().trace_id, '032x')
# Simulate processing
time.sleep(random.uniform(0.01, 0.05))
if item_id < 0 or item_id >= len(ITEMS):
logger.warning("Item not found", extra={
'trace_id': trace_id,
'item_id': item_id
})
return jsonify({'error': 'Item not found', 'trace_id': trace_id}), 404
item = ITEMS[item_id]
logger.info("Item fetched", extra={
'trace_id': trace_id,
'item_id': item_id,
'item': item
})
return jsonify({
'id': item_id,
'name': item,
'trace_id': trace_id
})
@app.route('/slow')
def slow_endpoint():
with tracer.start_as_current_span("slow_operation") as span:
trace_id = format(span.get_span_context().trace_id, '032x')
logger.info("Slow operation started", extra={'trace_id': trace_id})
# Simulate slow operation
time.sleep(random.uniform(1, 3))
logger.info("Slow operation completed", extra={'trace_id': trace_id})
return jsonify({
'message': 'Operation completed',
'trace_id': trace_id
})
@app.route('/error')
def error_endpoint():
with tracer.start_as_current_span("error_operation") as span:
trace_id = format(span.get_span_context().trace_id, '032x')
logger.error("Intentional error triggered", extra={'trace_id': trace_id})
span.set_attribute("error", True)
return jsonify({
'error': 'This is an intentional error',
'trace_id': trace_id
}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
namespace: observability
labels:
app: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
containers:
- name: demo-app
image: python:3.11-slim
command:
- /bin/bash
- -c
- |
pip install flask opentelemetry-api opentelemetry-sdk \
opentelemetry-instrumentation-flask \
opentelemetry-exporter-otlp-proto-grpc \
prometheus-flask-exporter && \
python /app/app.py
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: app-code
mountPath: /app
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
volumes:
- name: app-code
configMap:
name: demo-app
---
apiVersion: v1
kind: Service
metadata:
name: demo-app
namespace: observability
labels:
app: demo-app
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: http
protocol: TCP
name: http
selector:
app: demo-app