Skip to content

Commit d9b97e7

Browse files
abbiesimsedisile
authored andcommitted
add local o11y
1 parent fd89ff9 commit d9b97e7

17 files changed

Lines changed: 295 additions & 0 deletions

File tree

.env

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ FLASK_DEBUG=true
44
DEVEL=True
55
SECRET_KEY=local_development_fake_key
66
LP_API_USERNAME=test_lp_user
7+
8+
# OpenTelemetry
9+
# Uncomment to enable tracing:
10+
OTEL_SERVICE_NAME=snapcraft-io
11+
# if using Linux, uncomment this:
12+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
13+
# if using MacOS, uncomment this:
14+
# OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318

observability/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Local Tracing for charmhub.io
2+
3+
This guide explains how to test OpenTelemetry traces locally using OpenTelemetry, Grafana and Tempo.
4+
5+
*This setup is intended for local development and testing purposes only.*
6+
7+
## Overview
8+
9+
This setup enables observability for local development by capturing traces from the application and visualising them in Grafana, using Tempo as the backend.
10+
11+
## Prerequisites
12+
13+
- Docker and Docker Compose installed
14+
- A properly configured `.env` file
15+
16+
## Setup Instructions
17+
18+
### 1. Configure environment variables
19+
20+
In your `.env` file, make the following changes:
21+
22+
- Uncomment the appropriate `OTEL_EXPORTER_OTLP_ENDPOINT` based on your operating system.
23+
- Uncomment `OTEL_SERVICE_NAME` to enable tracing.
24+
25+
### 2. Start observability stack
26+
27+
Open a new terminal window and run:
28+
29+
`cd observability`
30+
`docker compose up -d`
31+
32+
This starts the OpenTelemetry Collector, Grafana and Tempo containers for tracing.
33+
34+
### 3. Run the application
35+
36+
Back in the main terminal (in the root of the project), run `dotrun`
37+
38+
### 4. Generate traces
39+
40+
Interact with the application by visiting various pages such as:
41+
42+
- Homepage (list of charms)
43+
- Charm pages
44+
- Login page
45+
- Publisher pages
46+
47+
These interactions will generate traces.
48+
49+
### 5. View traces in Grafana
50+
51+
Open Grafana in the browser at: http://localhost:3000
52+
53+
Login using the default credentials:
54+
55+
- Username: `admin`
56+
- Password: `admin`
57+
58+
Then:
59+
1. Go to `Explore`
60+
2. The Tempo datasource should be selected
61+
3. Click `Search`
62+
4. You should see a list of trace IDs
63+
5. Click on a trace ID to view detailed nested spans
64+
65+
### 6. Stop the observability stack
66+
Once you're done testing, you can shut down the containers:
67+
`cd observability`
68+
`docker compose down`
69+

observability/docker-compose.yaml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
services:
2+
grafana:
3+
image: grafana/grafana:latest
4+
ports:
5+
- "3000:3000"
6+
volumes:
7+
- ./grafana/provisioning:/etc/grafana/provisioning
8+
depends_on:
9+
- tempo
10+
networks:
11+
- observability
12+
13+
prometheus:
14+
image: prom/prometheus:latest
15+
ports:
16+
- "9090:9090"
17+
volumes:
18+
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
19+
networks:
20+
- observability
21+
22+
tempo:
23+
image: grafana/tempo:latest
24+
ports:
25+
- "3200:3200" # Tempo UI/query
26+
command: [ "-config.file=/etc/tempo.yaml" ]
27+
volumes:
28+
- ./tempo/tempo.yaml:/etc/tempo.yaml
29+
networks:
30+
- observability
31+
32+
otel-collector:
33+
image: otel/opentelemetry-collector-contrib:latest
34+
command: [ "--config=/etc/otel-collector-config.yaml" ]
35+
ports:
36+
- "4318:4318" # OTLP HTTP - app on localhost sends traces here
37+
volumes:
38+
- ./otel-collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml
39+
depends_on:
40+
- tempo
41+
networks:
42+
- observability
43+
44+
networks:
45+
observability:
46+
driver: bridge
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
apiVersion: 1
2+
3+
datasources:
4+
- name: Tempo
5+
type: tempo
6+
access: proxy
7+
url: http://tempo:3200
8+
isDefault: true
9+
10+
- name: Prometheus
11+
type: prometheus
12+
access: proxy
13+
url: http://prometheus:9090
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
receivers:
2+
otlp:
3+
protocols:
4+
http:
5+
endpoint: "0.0.0.0:4318"
6+
7+
exporters:
8+
otlp:
9+
endpoint: tempo:4317
10+
tls:
11+
insecure: true
12+
13+
service:
14+
pipelines:
15+
traces:
16+
receivers: [otlp]
17+
exporters: [otlp]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
global:
2+
scrape_interval: 10s
3+
4+
scrape_configs:
5+
- job_name: 'snapcraft-io'
6+
metrics_path: '/_status/metrics'
7+
static_configs:
8+
- targets: ['snapcraft.io']

observability/tempo/tempo.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
auth_enabled: false
2+
3+
server:
4+
http_listen_port: 3200
5+
6+
distributor:
7+
receivers:
8+
otlp:
9+
protocols:
10+
grpc:
11+
endpoint: 0.0.0.0:4317
12+
ingester:
13+
trace_idle_period: 10s
14+
max_block_bytes: 5_000_000
15+
max_block_duration: 5m
16+
17+
compactor:
18+
compaction:
19+
block_retention: 1h
20+
21+
storage:
22+
trace:
23+
backend: local
24+
local:
25+
path: /tmp/tempo/traces

requirements.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,13 @@ Flask-Testing==0.8.1
3030
freezegun==1.4.0
3131
black==24.3.0
3232
flake8==6.1.0
33+
34+
# Observability
35+
opentelemetry-api==1.32.1
36+
opentelemetry-exporter-otlp==1.32.1
37+
opentelemetry-exporter-otlp-proto-http==1.32.1
38+
opentelemetry-instrumentation==0.53b1
39+
opentelemetry-instrumentation-wsgi==0.53b1
40+
opentelemetry-instrumentation-flask==0.53b1
41+
opentelemetry-instrumentation-requests==0.53b1
42+
opentelemetry-sdk==1.32.1

webapp/app.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,44 @@
2626
from webapp.tutorials.views import init_tutorials
2727
from webapp.packages.store_packages import store_packages
2828

29+
# --- OpenTelemetry imports ---
30+
from opentelemetry.instrumentation.flask import FlaskInstrumentor
31+
from opentelemetry.instrumentation.requests import RequestsInstrumentor
32+
from opentelemetry.trace import Span
33+
from webapp.observability.utils import trace_function
34+
from opentelemetry import trace
35+
from opentelemetry.sdk.trace import TracerProvider
36+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
37+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
38+
OTLPSpanExporter
39+
)
40+
from opentelemetry.sdk.resources import Resource
2941

3042
TALISKER_WSGI_LOGGER = logging.getLogger("talisker.wsgi")
3143

3244

45+
# OpenTelemetry
46+
UNTRACED_ROUTES = [
47+
"/_status",
48+
".*[.jpg|.jpeg|.png|.gif|.ico|.css|.js|.json]$",
49+
]
50+
51+
52+
# Setup tracing manually
53+
# could be removed if we can run dotrun with opentelemetry
54+
resource = Resource.create()
55+
trace.set_tracer_provider(TracerProvider(resource=resource))
56+
tracer_provider = trace.get_tracer_provider()
57+
otlp_exporter = OTLPSpanExporter() # reads env variables
58+
tracer_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
59+
60+
61+
@trace_function
62+
def request_hook(span: Span, environ):
63+
if span and span.is_recording():
64+
span.update_name(f"{environ['REQUEST_METHOD']} {environ['PATH_INFO']}")
65+
66+
3367
def create_app(testing=False):
3468
app = FlaskBase(
3569
__name__,
@@ -49,6 +83,13 @@ def create_app(testing=False):
4983
talisker.requests.configure(webapp.helpers.api_session)
5084
talisker.requests.configure(webapp.helpers.api_publisher_session)
5185

86+
# Add tracing auto instrumentation
87+
FlaskInstrumentor().instrument_app(
88+
app, excluded_urls=",".join(UNTRACED_ROUTES),
89+
request_hook=request_hook
90+
)
91+
RequestsInstrumentor().instrument()
92+
5293
if testing:
5394

5495
@app.context_processor

webapp/helpers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from webapp.api.requests import PublisherSession, Session
99
from canonicalwebteam.store_api.dashboard import Dashboard
1010
import webapp.api.marketo as marketo_api
11+
from webapp.observability.utils import trace_function
1112

1213
_yaml = YAML(typ="rt")
1314
_yaml_safe = YAML(typ="safe")
@@ -98,13 +99,15 @@ def dump_yaml(data, stream, typ="safe"):
9899
yaml.dump(data, stream)
99100

100101

102+
@trace_function
101103
def get_icon(media):
102104
icons = [m["url"] for m in media if m["type"] == "icon"]
103105
if len(icons) > 0:
104106
return icons[0]
105107
return ""
106108

107109

110+
@trace_function
108111
def get_publisher_data():
109112
# We don't use the data from this endpoint.
110113
# It is mostly used to make sure the user has signed
@@ -139,6 +142,7 @@ def get_publisher_data():
139142
return context
140143

141144

145+
@trace_function
142146
def get_dns_verification_token(snap_name, domain):
143147
salt = os.getenv("DNS_VERIFICATION_SALT")
144148
token_string = f"{domain}:{snap_name}:{salt}"

0 commit comments

Comments
 (0)