-
Notifications
You must be signed in to change notification settings - Fork 6
feat: production Docker hardening, /health/ readiness, and JSON logging #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
wpak-ai
merged 8 commits into
cppalliance:develop
from
leostar0412:feat/docker-health-and-observability
May 22, 2026
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
1d100bf
Enhance health checks and logging for collector service
leostar0412 9a5a30e
Remove healthcheck disable settings for celery services and implement…
leostar0412 cda8945
Refactor docker-entrypoint.sh to optimize ownership changes and enhan…
leostar0412 3f4a831
Enhance health check logic to support optional Bearer token for autho…
leostar0412 83e30d9
Merge branch 'develop' into feat/docker-health-and-observability
leostar0412 bf8683d
Enhance health check logic to support optional Bearer token for autho…
leostar0412 2676cb0
Fix syntax in Dockerfile health check command to ensure proper variab…
leostar0412 eebb4f5
Enhance health check response to include detailed collector metadata,…
leostar0412 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,45 +1,50 @@ | ||
| # Boost Data Collector - Docker image | ||
| # Same image runs: web (gunicorn), celery worker, celery beat | ||
|
|
||
| FROM python:3.11-slim | ||
| FROM python:3.13-slim | ||
|
|
||
| # Prevent Python from writing .pyc and buffering stdout/stderr | ||
| ENV PYTHONDONTWRITEBYTECODE=1 | ||
| ENV PYTHONUNBUFFERED=1 | ||
| ENV DJANGO_SETTINGS_MODULE=config.settings | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| # Install system deps (PostgreSQL client libs for psycopg, git for github_ops, gosu for entrypoint) | ||
| # System deps: PostgreSQL client, git, curl (HEALTHCHECK), gosu (dev entrypoint only). | ||
| # Pinned to Debian 13 (trixie) versions from python:3.13-slim at pin time; refresh with: | ||
| # docker run --rm python:3.13-slim bash -c 'apt-get update -qq && for p in libpq5 git curl gosu; do echo -n "$p="; apt-cache policy "$p" | awk "/Candidate:/{print \$2; exit}"; done' | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| libpq5 \ | ||
| git \ | ||
| gosu \ | ||
| libpq5=17.10-0+deb13u1 \ | ||
| git=1:2.47.3-0+deb13u1 \ | ||
| curl=8.14.1-2+deb13u3 \ | ||
| gosu=1.17-3+b4 \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # Install Python dependencies (fully pinned lockfile; gunicorn included in lock) | ||
| COPY requirements.lock . | ||
| RUN pip install --no-cache-dir -r requirements.lock | ||
|
|
||
| # Copy project code | ||
| COPY . . | ||
|
|
||
| # Create dirs that Django/settings expect (logs, staticfiles, workspace, celerybeat) | ||
| RUN mkdir -p logs staticfiles workspace celerybeat | ||
|
|
||
| # Entrypoint fixes volume permissions then runs CMD as appuser | ||
| COPY docker-entrypoint.sh /app/docker-entrypoint.sh | ||
| RUN chmod +x /app/docker-entrypoint.sh | ||
|
|
||
| # Entrypoint runs as root, chowns mounted dirs, then exec's CMD as appuser via gosu | ||
| RUN useradd --create-home appuser && chown -R appuser /app | ||
| # Git 2.35+ blocks repos when directory owner != current user; bind mounts often | ||
| # disagree (e.g. Docker Desktop on Windows). System config applies to root and appuser | ||
| # (e.g. docker exec as root vs gosu appuser in entrypoint). | ||
| RUN groupadd --gid 10001 appuser \ | ||
| && useradd --uid 10001 --gid 10001 --create-home appuser \ | ||
| && chown -R appuser:appuser /app | ||
| RUN git config --system --add safe.directory '/app/workspace/*' | ||
| ENTRYPOINT ["/app/docker-entrypoint.sh"] | ||
| # Container starts as root so entrypoint can chown; CMD runs as appuser via gosu | ||
|
|
||
| # Default: run gunicorn (overridden in docker-compose for worker/beat) | ||
| USER appuser | ||
|
|
||
| EXPOSE 8000 | ||
| CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "config.wsgi:application"] | ||
|
|
||
| # When HEALTH_CHECK_TOKEN is set (runtime env from compose/.env), send Bearer auth. | ||
| HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ | ||
| CMD sh -c 'if [ -n "${HEALTH_CHECK_TOKEN:-}" ]; then \ | ||
| curl -fsS -H "Authorization: Bearer ${HEALTH_CHECK_TOKEN}" http://127.0.0.1:8000/health/; \ | ||
| else \ | ||
| curl -fsS http://127.0.0.1:8000/health/; \ | ||
| fi' | ||
|
|
||
| ENTRYPOINT ["/app/docker-entrypoint.sh"] | ||
| CMD ["gunicorn", "-c", "docker/gunicorn.conf.py", "config.wsgi:application"] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Generated manually for production health tracking | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| initial = True | ||
|
|
||
| dependencies = [] | ||
|
|
||
| operations = [ | ||
| migrations.CreateModel( | ||
| name="CollectorGroupRunStatus", | ||
| fields=[ | ||
| ( | ||
| "group_id", | ||
| models.CharField(max_length=64, primary_key=True, serialize=False), | ||
| ), | ||
| ("last_success_at", models.DateTimeField(blank=True, null=True)), | ||
| ("last_failure_at", models.DateTimeField(blank=True, null=True)), | ||
| ("last_run_at", models.DateTimeField(blank=True, null=True)), | ||
| ("last_exit_code", models.IntegerField(blank=True, null=True)), | ||
| ("updated_at", models.DateTimeField(auto_now=True)), | ||
| ], | ||
| options={ | ||
| "verbose_name": "Collector group run status", | ||
| "verbose_name_plural": "Collector group run statuses", | ||
| "db_table": "boost_collector_runner_collectorgrouprunstatus", | ||
| }, | ||
| ), | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,22 @@ | ||
| # Boost collector runner has no models; it only runs management commands. | ||
| """Models for scheduled collector group run tracking.""" | ||
|
|
||
| from django.db import models | ||
|
|
||
|
|
||
| class CollectorGroupRunStatus(models.Model): | ||
| """Last run outcome per YAML schedule group (e.g. github, slack).""" | ||
|
|
||
| group_id = models.CharField(max_length=64, primary_key=True) | ||
| last_success_at = models.DateTimeField(null=True, blank=True) | ||
| last_failure_at = models.DateTimeField(null=True, blank=True) | ||
| last_run_at = models.DateTimeField(null=True, blank=True) | ||
| last_exit_code = models.IntegerField(null=True, blank=True) | ||
| updated_at = models.DateTimeField(auto_now=True) | ||
|
|
||
| class Meta: | ||
| db_table = "boost_collector_runner_collectorgrouprunstatus" | ||
| verbose_name = "Collector group run status" | ||
| verbose_name_plural = "Collector group run statuses" | ||
|
|
||
| def __str__(self) -> str: | ||
| return f"CollectorGroupRunStatus(group_id={self.group_id})" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| """ | ||
| Service layer for boost_collector_runner. | ||
|
|
||
| All creates/updates for this app's models must go through functions in this module. | ||
| See CONTRIBUTING.md. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from datetime import datetime | ||
| from typing import Optional | ||
|
|
||
| from django.utils import timezone | ||
|
|
||
| from .models import CollectorGroupRunStatus | ||
|
|
||
|
|
||
| def record_group_success( | ||
| group_id: str, *, when: Optional[datetime] = None | ||
| ) -> CollectorGroupRunStatus: | ||
| """Record a successful group batch run.""" | ||
| ts = when or timezone.now() | ||
| obj, _created = CollectorGroupRunStatus.objects.update_or_create( | ||
| group_id=group_id, | ||
| defaults={ | ||
| "last_success_at": ts, | ||
| "last_run_at": ts, | ||
| "last_exit_code": 0, | ||
| }, | ||
| ) | ||
| return obj | ||
|
|
||
|
|
||
| def record_group_failure( | ||
| group_id: str, | ||
| *, | ||
| exit_code: int = 1, | ||
| when: Optional[datetime] = None, | ||
| ) -> CollectorGroupRunStatus: | ||
| """Record a failed group batch run.""" | ||
| ts = when or timezone.now() | ||
| obj, _created = CollectorGroupRunStatus.objects.update_or_create( | ||
| group_id=group_id, | ||
| defaults={ | ||
| "last_failure_at": ts, | ||
| "last_run_at": ts, | ||
| "last_exit_code": exit_code, | ||
| }, | ||
| ) | ||
| return obj | ||
|
|
||
|
|
||
| def get_group_status(group_id: str) -> Optional[CollectorGroupRunStatus]: | ||
| """Return status row for a group, or None if never run.""" | ||
| return CollectorGroupRunStatus.objects.filter(group_id=group_id).first() | ||
|
|
||
|
|
||
| def list_group_statuses() -> dict[str, CollectorGroupRunStatus]: | ||
| """Return all group statuses keyed by group_id.""" | ||
| return {row.group_id: row for row in CollectorGroupRunStatus.objects.all()} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| """Tests for boost_collector_runner.services.""" | ||
|
|
||
| import pytest | ||
| from django.utils import timezone | ||
|
|
||
| from boost_collector_runner import services | ||
| from boost_collector_runner.models import CollectorGroupRunStatus | ||
|
|
||
| pytestmark = pytest.mark.django_db | ||
|
|
||
|
|
||
| def test_record_group_success_creates_row(): | ||
| when = timezone.now() | ||
| row = services.record_group_success("github", when=when) | ||
| assert row.group_id == "github" | ||
| assert row.last_success_at == when | ||
| assert row.last_exit_code == 0 | ||
| assert CollectorGroupRunStatus.objects.filter(group_id="github").exists() | ||
|
|
||
|
|
||
| def test_record_group_failure_sets_exit_code(): | ||
| when = timezone.now() | ||
| row = services.record_group_failure("slack", exit_code=2, when=when) | ||
| assert row.last_failure_at == when | ||
| assert row.last_exit_code == 2 | ||
|
|
||
|
|
||
| def test_list_group_statuses(): | ||
| services.record_group_success("github") | ||
| statuses = services.list_group_statuses() | ||
| assert "github" in statuses |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.