Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6d9421d
WIP
Hyask Mar 3, 2026
f0edb00
fix(errors): replace Python 2 __getslice__ pattern with direct obj_ge…
Copilot Apr 2, 2026
84f89db
revert: undo WIP and its follow-up fix to re-apply as clean commits
Copilot Apr 2, 2026
3a79470
docs: add mention of populate-test-data.sh script and update test arc…
Copilot Apr 2, 2026
04570ea
fix(retracer): add trusted=yes flag to Ubuntu 26.04 ddebs sources
Copilot Apr 2, 2026
c8eb8aa
fix(launchpad): fix indentation bug in pocket_for_binaries and remove…
Copilot Apr 2, 2026
8101218
refactor(errors): modernize Django settings, WSGI, and remove depreca…
Copilot Apr 2, 2026
a24ce0a
fix(errors): replace Python 2 __getslice__ pattern in all API resources
Copilot Apr 2, 2026
f877669
fix(errors/js): add null guards and remove server-side means data fro…
Copilot Apr 2, 2026
1f52e20
errors: static: import all missing js libraries from old repo
Hyask Apr 22, 2026
27bfe8d
Add 'populate-test-data.sh'
Hyask Apr 30, 2026
fb2c158
errors: pygmentize: make sure to output 'str' instead of 'bytes'
Hyask May 4, 2026
600ced5
errors: bucket: fix 'allow_bug_filing' variable not existing in JS
Hyask May 4, 2026
0fd6317
errors: more fixes
Hyask May 4, 2026
2314c79
refactor: replace shell scripts in src/ with a single Makefile
Copilot May 4, 2026
d920e57
feat(errors): add OpenID Teams extension to sync Launchpad team membe…
Copilot May 4, 2026
24548d5
errors: simplify auth file structure
Hyask May 5, 2026
8ce875f
errors: simplify handling of static files
Hyask May 5, 2026
6db7463
errors: settings: do some clean up
Hyask May 5, 2026
e30e186
errors: basic kickstart of a working charm part
Hyask May 5, 2026
1e46ca0
errors: call 'setup_cassandra' in a way compatible with uWSGI
Hyask May 6, 2026
03f9389
Makefile: make it better: more rules more betterz
Hyask May 6, 2026
b0e773a
gitignore: add Django's 'collectstatic' folder
Hyask May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 2 additions & 4 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,10 @@ src/ # Error Tracker application source
amqp_utils.py # RabbitMQ/AMQP utilities
config.py # Configuration handling
utils.py # Shared utilities
Makefile # Makefile with targets: run-daisy, run-errors, run-retracer, populate-test-data
retracer/ # Symbolic retracer (turns addresses into stack frames)
config/ # Per-release retracer configuration
retracer.py # Retracer entry point
run-daisy.sh # Script to start daisy locally
run-errors.sh # Script to start errors locally
run-retracer.sh # Script to start retracer locally
tools/ # Maintenance and housekeeping scripts
tests/ # Application tests
Expand Down Expand Up @@ -101,7 +99,7 @@ The application relies on:

### Test Architecture

There are two layers of testing:
There are multiple layers of testing:

1. **Unit/functional tests** (`src/tests/`) — pytest-based tests that require Cassandra, RabbitMQ, and Swift running locally. These test individual components (submission, retracing, Cassandra operations, OOPS processing).

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ cover/
local_config.py
db.sqlite3
db.sqlite3-journal
src/static

# Flask stuff:
instance/
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Start with the Python dependencies
# For 'daisy' only
sudo apt install apport-retrace python3-amqp python3-bson python3-cassandra python3-flask python3-mock python3-pygit2 python3-pytest python3-pytest-cov python3-swiftclient ubuntu-dbgsym-keyring
# Add this for 'errors'
sudo apt install python3-django-tastypie python3-numpy python3-social-django
sudo apt install python3-django-tastypie python3-numpy python3-social-django python3-openid-teams
```

Then start a local Cassandra, RabbitMQ and swift (`docker` should work fine too):
Expand All @@ -48,19 +48,21 @@ Or start each individual process (from the `./src` folder):

daisy:
```
./run-daisy.sh
make daisy-run
```

retracer:
```
./run-retracer.sh
make retracer-run
```

errors:
```
./run-errors.sh
make errors-run
```

If you need test data in Cassandra, you can run `make populate-test-data` in that same folder.

From there, you can manually upload a crash with the following, from any folder
containing a `.crash` file with its corresponding `.upload` file:
```
Expand Down
11 changes: 10 additions & 1 deletion charm/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ def __init__(self, *args):
ports=[self._error_tracker.daisy_port],
relation_name="route_daisy",
)
self.route_web = HaproxyRouteRequirer(
self,
service="web",
ports=[self._error_tracker.web_port],
relation_name="route_web",
)

self.framework.observe(self.on.start, self._on_start)
self.framework.observe(self.on.install, self._on_install)
Expand Down Expand Up @@ -62,15 +68,18 @@ def _on_config_changed(self, event: ops.ConfigChangedEvent):
# This is a bit annoying, but also doesn't have a very big impact in
# practice. This charm has no configuration where it's supposed to store
# data, so it's always very easy to remove a unit and recreate.
ports = []
if enable_daisy:
self._error_tracker.configure_daisy()
self.unit.set_ports(self._error_tracker.daisy_port)
ports.append(self._error_tracker.daisy_port)
if enable_retracer:
self._error_tracker.configure_retracer(self.config.get("retracer_failed_queue"))
if enable_timers:
self._error_tracker.configure_timers()
if enable_web:
self._error_tracker.configure_web()
ports.append(self._error_tracker.web_port)
self.unit.set_ports(*ports)

self.unit.set_workload_version(self._error_tracker.get_version())
self.unit.status = ops.ActiveStatus("Ready")
Expand Down
45 changes: 45 additions & 0 deletions charm/errortracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def __init__(self):
self.enable_daisy = True
self.enable_web = True
self.daisy_port = 8000
self.web_port = 9000

def install(self):
self._install_deps()
Expand Down Expand Up @@ -238,3 +239,47 @@ def configure_timers(self):

def configure_web(self):
logger.info("Configuring web")
logger.info("Installing additional web dependencies")
check_call(
[
"apt-get",
"install",
"-y",
"python3-django",
"python3-django-tastypie",
"python3-numpy",
"python3-openid-teams",
"python3-social-django",
"python3-uwsgidecorators",
"uwsgi-plugin-python3",
]
)
systemd_unit_location = Path("/") / "etc" / "systemd" / "system"
systemd_unit_location.mkdir(parents=True, exist_ok=True)
(systemd_unit_location / "et-web.service").write_text(
f"""
[Unit]
Description=Error Tracker web
After=network.target

[Service]
User=ubuntu
Group=ubuntu
WorkingDirectory={REPO_LOCATION}/src
ExecStartPre={REPO_LOCATION}/src/errors/manage.py migrate --no-input
ExecStartPre={REPO_LOCATION}/src/errors/manage.py collectstatic --no-input --clear
ExecStart=bash -c 'exec uwsgi --plugins python3 --http-socket 0.0.0.0:{self.web_port} --wsgi-file {REPO_LOCATION}/src/errors/wsgi.py --static-map "/static={REPO_LOCATION}/src/static/" --chdir {REPO_LOCATION}/src/ --die-on-term --master --env PYTHONPATH={REPO_LOCATION}/src/ --max-requests 4000 --max-worker-lifetime 21600 --processes "$(($(nproc) * 2))"'
Restart=always

[Install]
WantedBy=multi-user.target
"""
)

check_call(["systemctl", "daemon-reload"])

logger.info("enabling systemd units")
check_call(["systemctl", "enable", "et-web"])

logger.info("restarting systemd units")
check_call(["systemctl", "restart", "et-web"])
4 changes: 2 additions & 2 deletions charm/tests/integration/test_daisy.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ def test_deploy(


def test_http(juju: jubilant.Juju):
juju.deploy(HAPROXY, channel="2.8/edge", config={"external-hostname": "daisy.internal"})
external_hostname = "daisy.internal"
juju.deploy(HAPROXY, channel="2.8/edge", config={"external-hostname": external_hostname})
juju.deploy(SSC, channel="1/edge")

juju.integrate(HAPROXY + ":certificates", SSC + ":certificates")
juju.integrate("daisy:route_daisy", HAPROXY)
juju.wait(lambda status: jubilant.all_active(status, HAPROXY, SSC), timeout=1800)

haproxy_ip = juju.status().apps[HAPROXY].units[f"{HAPROXY}/0"].public_address
external_hostname = "daisy.internal"

session = Session()
session.mount("https://", DNSResolverHTTPSAdapter(external_hostname, haproxy_ip))
Expand Down
68 changes: 68 additions & 0 deletions charm/tests/integration/test_web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import logging

import jubilant
from requests import Session
from tenacity import Retrying, stop_after_attempt, wait_exponential
from utils import DNSResolverHTTPSAdapter, check_config

logger = logging.getLogger()

HAPROXY = "haproxy"
SSC = "self-signed-certificates"


def test_deploy(
juju: jubilant.Juju,
amqp: dict[str, str],
cassandra: dict[str, str],
swift: dict[str, str],
error_tracker_config: str,
charm_path: str,
):
juju.deploy(
charm=charm_path,
app="web",
config={
"configuration": error_tracker_config,
"enable_daisy": False,
"enable_retracer": False,
"enable_timers": False,
"enable_web": True,
},
)

juju.wait(lambda status: jubilant.all_active(status, "web"), timeout=600)

check_config(juju, amqp, cassandra, swift, "web/0")


def test_http(juju: jubilant.Juju):
external_hostname = "errors.internal"
juju.deploy(HAPROXY, channel="2.8/edge", config={"external-hostname": external_hostname})
juju.deploy(SSC, channel="1/edge")

juju.integrate(HAPROXY + ":certificates", SSC + ":certificates")
juju.integrate("web:route_web", HAPROXY)
juju.wait(lambda status: jubilant.all_active(status, HAPROXY, SSC), timeout=1800)

haproxy_ip = juju.status().apps[HAPROXY].units[f"{HAPROXY}/0"].public_address

session = Session()
session.mount("https://", DNSResolverHTTPSAdapter(external_hostname, haproxy_ip))

# Let give this test a few chances to succeed, as it can sometimes be a bit
# early and hit 503
for attempt in Retrying(
stop=stop_after_attempt(10),
wait=wait_exponential(min=5, max=30),
reraise=True,
):
with attempt:
response = session.get(
f"https://{haproxy_ip}/",
headers={"Host": external_hostname},
verify=False,
timeout=30,
)
assert response.status_code == 200
assert "We collect hundreds of thousands of error reports daily" in response.text
4 changes: 4 additions & 0 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,7 @@ requires:
interface: haproxy-route
limit: 1
optional: true
route_web:
interface: haproxy-route
limit: 1
optional: true
25 changes: 25 additions & 0 deletions src/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
BASE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))

export PYTHONPATH := $(BASE_DIR)

.PHONY: daisy-run errors-run errors-shell retracer-run populate-test-data

daisy-run:
python3 $(BASE_DIR)daisy/app.py

errors-run:
python3 $(BASE_DIR)errors/manage.py migrate
python3 $(BASE_DIR)errors/manage.py runserver

errors-uwsgi:
python3 $(BASE_DIR)errors/manage.py migrate
uwsgi --plugins python3 --http-socket 0.0.0.0:9000 --wsgi-file $(BASE_DIR)errors/wsgi.py --static-map "/static=$(BASE_DIR)../static/" --chdir $(BASE_DIR) --die-on-term --master --env PYTHONPATH=$(BASE_DIR)

errors-shell:
python3 $(BASE_DIR)errors/manage.py shell

retracer-run:
python3 $(BASE_DIR)retracer.py -a amd64 --sandbox-dir /tmp/sandbox -v --config-dir $(BASE_DIR)retracer/config

populate-test-data:
python3 $(BASE_DIR)tests/create_test_data.py
Loading
Loading