Skip to content

Commit 57563df

Browse files
authored
Merge pull request #756 from pnu-code-place/release-3.1.5
Release 3.1.5
2 parents aca4bd5 + c520d14 commit 57563df

66 files changed

Lines changed: 5229 additions & 998 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci2develop.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,20 @@ jobs:
6565
runs-on: ubuntu-latest
6666
steps:
6767
- name: Checkout code
68-
uses: actions/checkout@v2
68+
uses: actions/checkout@v6
6969

7070
- name: Set up Docker Buildx
71-
uses: docker/setup-buildx-action@v1
71+
uses: docker/setup-buildx-action@v4
7272

7373
- name: Login to Harbor
74-
uses: docker/login-action@v1
74+
uses: docker/login-action@v4
7575
with:
7676
registry: ${{ secrets.HARBOR_REGISTRY }}
7777
username: ${{ secrets.HARBOR_USERNAME }}
7878
password: ${{ secrets.HARBOR_PASSWORD }}
7979

8080
- name: Build and push Docker image
81-
uses: docker/build-push-action@v2
81+
uses: docker/build-push-action@v7
8282
with:
8383
context: ./frontend
8484
file: ./frontend/Dockerfile
@@ -91,6 +91,8 @@ jobs:
9191
SENTRY_ENVIRONMENT=development
9292
SENTRY_DSN_FRONTEND=${{ secrets.SENTRY_DSN_FRONTEND }}
9393
USE_SENTRY=1
94+
secrets: |
95+
"sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }}"
9496
9597
ci-hub-auth-dev:
9698
needs: [detect-changes-by-component]

.github/workflows/ci2production-and-release.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,20 +93,20 @@ jobs:
9393
runs-on: ubuntu-latest
9494
steps:
9595
- name: Checkout code
96-
uses: actions/checkout@v2
96+
uses: actions/checkout@v6
9797

9898
- name: Set up Docker Buildx
99-
uses: docker/setup-buildx-action@v1
99+
uses: docker/setup-buildx-action@v4
100100

101101
- name: Login to Harbor
102-
uses: docker/login-action@v1
102+
uses: docker/login-action@v4
103103
with:
104104
registry: ${{ secrets.HARBOR_REGISTRY }}
105105
username: ${{ secrets.HARBOR_USERNAME }}
106106
password: ${{ secrets.HARBOR_PASSWORD }}
107107

108108
- name: Build and push Docker image
109-
uses: docker/build-push-action@v2
109+
uses: docker/build-push-action@v7
110110
with:
111111
context: ./frontend
112112
file: ./frontend/Dockerfile
@@ -119,6 +119,8 @@ jobs:
119119
SENTRY_ENVIRONMENT=production
120120
SENTRY_DSN_FRONTEND=${{ secrets.SENTRY_DSN_FRONTEND }}
121121
USE_SENTRY=1
122+
secrets: |
123+
"sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }}"
122124
123125
ci-hub-auth:
124126
needs: [detect-changes-by-component]
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
name: Validate monitoring contracts
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "kubernetes/monitoring/**"
7+
- "kubernetes/base/hub-auth/**"
8+
- "kubernetes/base/backend/**"
9+
- "kubernetes/base/celery-beat/**"
10+
- "kubernetes/base/celery-worker/**"
11+
- "kubernetes/overlays/dev/**"
12+
- "kubernetes/overlays/prod/**"
13+
- "hub/auth_server/**"
14+
- "backend/account/middleware.py"
15+
- "backend/account/tests.py"
16+
- "backend/judge/tasks.py"
17+
- "backend/judge/tests.py"
18+
- "backend/oj/celery.py"
19+
- "backend/problem/llm_hint.py"
20+
- "backend/problem/views/oj.py"
21+
- "backend/problem/tests.py"
22+
- "backend/submission/views/oj.py"
23+
- "backend/submission/tests.py"
24+
- "backend/utils/observability_metrics.py"
25+
- "backend/utils/observability_tracing.py"
26+
- "backend/utils/tests.py"
27+
- "backend/utils/test_observability_tracing.py"
28+
- ".github/workflows/monitoring-validate.yml"
29+
push:
30+
branches: [develop, main]
31+
paths:
32+
- "kubernetes/monitoring/**"
33+
- "kubernetes/base/hub-auth/**"
34+
- "kubernetes/base/backend/**"
35+
- "kubernetes/base/celery-beat/**"
36+
- "kubernetes/base/celery-worker/**"
37+
- "kubernetes/overlays/dev/**"
38+
- "kubernetes/overlays/prod/**"
39+
- "hub/auth_server/**"
40+
- "backend/account/middleware.py"
41+
- "backend/account/tests.py"
42+
- "backend/judge/tasks.py"
43+
- "backend/judge/tests.py"
44+
- "backend/oj/celery.py"
45+
- "backend/problem/llm_hint.py"
46+
- "backend/problem/views/oj.py"
47+
- "backend/problem/tests.py"
48+
- "backend/submission/views/oj.py"
49+
- "backend/submission/tests.py"
50+
- "backend/utils/observability_metrics.py"
51+
- "backend/utils/observability_tracing.py"
52+
- "backend/utils/tests.py"
53+
- "backend/utils/test_observability_tracing.py"
54+
- ".github/workflows/monitoring-validate.yml"
55+
56+
permissions:
57+
contents: read
58+
59+
jobs:
60+
validate:
61+
runs-on: ubuntu-latest
62+
env:
63+
POSTGRES_HOST: 127.0.0.1
64+
POSTGRES_PORT: "5435"
65+
POSTGRES_DB: onlinejudge
66+
POSTGRES_USER: onlinejudge
67+
POSTGRES_PASSWORD: onlinejudge
68+
REDIS_HOST: 127.0.0.1
69+
REDIS_PORT: "6380"
70+
services:
71+
postgres:
72+
image: postgres:14
73+
env:
74+
POSTGRES_DB: onlinejudge
75+
POSTGRES_USER: onlinejudge
76+
POSTGRES_PASSWORD: onlinejudge
77+
ports:
78+
- 5435:5432
79+
options: >-
80+
--health-cmd "pg_isready -U onlinejudge -d onlinejudge"
81+
--health-interval 10s
82+
--health-timeout 5s
83+
--health-retries 5
84+
redis:
85+
image: redis:7-alpine
86+
ports:
87+
- 6380:6379
88+
options: >-
89+
--health-cmd "redis-cli ping"
90+
--health-interval 10s
91+
--health-timeout 5s
92+
--health-retries 5
93+
steps:
94+
- name: Checkout repository
95+
uses: actions/checkout@v6
96+
97+
- name: Set up Python
98+
uses: actions/setup-python@v6
99+
with:
100+
python-version: "3.11"
101+
102+
- name: Set up kubectl
103+
uses: azure/setup-kubectl@v4
104+
with:
105+
version: "v1.33.6"
106+
107+
- name: Set up Helm
108+
uses: azure/setup-helm@v4
109+
with:
110+
version: "v3.18.6"
111+
112+
- name: Install validation dependencies
113+
run: python -m pip install --disable-pip-version-check -r backend/deploy/requirements.txt -r hub/auth_server/requirements.txt pytest PyYAML
114+
115+
- name: Test hub-auth health contract
116+
working-directory: hub/auth_server
117+
env:
118+
GITHUB_OAUTH_APP_ID: test-client
119+
GITHUB_OAUTH_APP_SECRET: test-secret
120+
run: python -m pytest app/test_main.py
121+
122+
- name: Test trace resource contract
123+
env:
124+
PYTHONPATH: backend
125+
run: python -m unittest utils.test_observability_tracing
126+
127+
- name: Test backend observability contracts
128+
working-directory: backend
129+
run: |
130+
printf 'monitoring-validation-secret\n' >data/config/secret.key
131+
python manage.py test \
132+
utils.tests.CodePlaceCollectorTest \
133+
utils.tests.CeleryRequestIDContextTest \
134+
utils.tests.ObservabilityTracingTest \
135+
utils.tests.CodePlaceMetricsEndpointTest \
136+
judge.tests.JudgeTaskObservabilityTest \
137+
account.tests.RequestLogMiddlewareTest \
138+
problem.tests.ProblemLLMHintAPITest.test_stream_llm_hint_records_api_success_outcome \
139+
problem.tests.ProblemLLMHintAPITest.test_stream_llm_hint_records_success_metric \
140+
problem.tests.ProblemLLMHintAPITest.test_stream_llm_hint_records_request_error_metric \
141+
problem.tests.ProblemLLMHintAPITest.test_stream_llm_hint_records_problem_limit_outcome \
142+
submission.tests.SubmissionAPITest.test_create_submission_records_success_outcome \
143+
submission.tests.SubmissionAPITest.test_create_submission_records_problem_not_found_outcome
144+
145+
- name: Validate dashboard contracts
146+
run: python kubernetes/monitoring/validate_dashboards.py --prometheus-rules-output /tmp/codeplace-dashboard-rules.json
147+
148+
- name: Validate alert contracts
149+
run: python kubernetes/monitoring/validate_alerts.py
150+
151+
- name: Render Kubernetes manifests
152+
run: |
153+
kubectl kustomize kubernetes/overlays/dev >/dev/null
154+
kubectl kustomize kubernetes/overlays/prod >/dev/null
155+
kubectl kustomize kubernetes/monitoring >/tmp/codeplace-monitoring.yaml
156+
python -c 'import pathlib, yaml; docs=[doc for doc in yaml.safe_load_all(pathlib.Path("/tmp/codeplace-monitoring.yaml").read_text()) if doc]; assert any(doc.get("kind") == "PodMonitor" and doc.get("metadata", {}).get("name") == "traefik" for doc in docs), "Traefik PodMonitor is missing"; assert not any(doc.get("kind") == "ServiceMonitor" and doc.get("metadata", {}).get("name") == "traefik" for doc in docs), "legacy Traefik ServiceMonitor remains"'
157+
test ! -e kubernetes/monitoring/traefik-service-monitor.yaml
158+
159+
- name: Render pinned monitoring charts
160+
run: |
161+
helm template kube-prometheus-stack kube-prometheus-stack \
162+
--repo https://prometheus-community.github.io/helm-charts \
163+
--version 86.3.1 \
164+
--namespace monitoring \
165+
--kube-version 1.33.6 \
166+
--values kubernetes/monitoring/kube-prometheus-stack-values.yaml >/tmp/kube-prometheus-stack.yaml
167+
python -c 'import pathlib, yaml; docs=[doc for doc in yaml.safe_load_all(pathlib.Path("/tmp/kube-prometheus-stack.yaml").read_text()) if doc]; assert any(doc.get("kind") == "Job" and doc.get("metadata", {}).get("name") == "kube-prometheus-stack-crds-upgrade" for doc in docs), "CRD upgrade Job is missing"; grafana=next(doc for doc in docs if doc.get("kind") == "Deployment" and doc.get("metadata", {}).get("name") == "kube-prometheus-stack-grafana"); assert grafana.get("spec", {}).get("strategy", {}).get("type") == "Recreate", "Grafana must use Recreate with its RWO PVC"'
168+
helm template loki loki \
169+
--repo https://grafana.github.io/helm-charts \
170+
--version 6.55.0 \
171+
--namespace monitoring \
172+
--kube-version 1.33.6 \
173+
--api-versions monitoring.coreos.com/v1/ServiceMonitor \
174+
--values kubernetes/monitoring/logs/loki-values.yaml >/dev/null
175+
helm template alloy alloy \
176+
--repo https://grafana.github.io/helm-charts \
177+
--version 1.10.0 \
178+
--namespace monitoring \
179+
--kube-version 1.33.6 \
180+
--api-versions monitoring.coreos.com/v1/ServiceMonitor \
181+
--values kubernetes/monitoring/logs/alloy-values.yaml >/dev/null
182+
183+
- name: Validate Prometheus rules
184+
run: |
185+
python -c 'import pathlib, yaml; source=yaml.safe_load(pathlib.Path("kubernetes/monitoring/prometheus-rules.yaml").read_text()); pathlib.Path("/tmp/codeplace-rules.yaml").write_text(yaml.safe_dump({"groups": source["spec"]["groups"]}))'
186+
docker run --rm --entrypoint /bin/promtool -v /tmp/codeplace-rules.yaml:/rules.yaml:ro prom/prometheus:v3.7.3 check rules /rules.yaml
187+
docker run --rm --entrypoint /bin/promtool -v /tmp/codeplace-dashboard-rules.json:/rules.json:ro prom/prometheus:v3.7.3 check rules /rules.json
188+
docker run --rm --entrypoint /bin/promtool -v /tmp/codeplace-rules.yaml:/rules.yaml:ro -v "$PWD/kubernetes/monitoring/prometheus-rules.test.yaml:/rules.test.yaml:ro" prom/prometheus:v3.7.3 test rules /rules.test.yaml

backend/account/tests.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from .models import AdminType, ProblemPermission, User
2020
from .decorators import login_required, scheduler_only
21-
from .middleware import AdminRoleRequiredMiddleware, RequestIDMiddleware
21+
from .middleware import AdminRoleRequiredMiddleware, RequestIDMiddleware, RequestLogMiddleware
2222
from .tasks import calculate_user_score_basis, calculate_user_score_fluctuation
2323

2424

@@ -65,6 +65,23 @@ def test_generates_request_id_when_header_has_no_safe_characters(self):
6565
self.assertEqual(response["X-Request-ID"], request.request_id)
6666

6767

68+
class RequestLogMiddlewareTest(SimpleTestCase):
69+
70+
@mock.patch("account.middleware.HTTP_REQUEST_DURATION_SECONDS")
71+
@mock.patch("account.middleware.HTTP_REQUESTS_TOTAL")
72+
def test_records_request_metrics(self, requests_total, duration_seconds):
73+
request = RequestFactory().get("/api/problem")
74+
request.resolver_match = mock.Mock(view_name="problem_api")
75+
middleware = RequestLogMiddleware(lambda _: JsonResponse({}, status=201))
76+
77+
middleware(request)
78+
79+
requests_total.labels.assert_called_once_with("GET", "problem_api", "201")
80+
requests_total.labels.return_value.inc.assert_called_once()
81+
duration_seconds.labels.assert_called_once_with("GET", "problem_api")
82+
duration_seconds.labels.return_value.observe.assert_called_once()
83+
84+
6885
class AdminRoleRequiredMiddlewareTest(SimpleTestCase):
6986

7087
def setUp(self):

backend/judge/tasks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from account.models import User
55
from utils.shortcuts import CELERY_TASK_ARGS
66
from utils.observability_tracing import get_tracer
7-
from utils.observability_metrics import JUDGE_TASK_OUTCOME_TOTAL
7+
from utils.observability_metrics import record_judge_task_outcome
88
from submission.models import Submission
99
from judge.dispatcher import JudgeDispatcher
1010

@@ -13,7 +13,7 @@
1313

1414

1515
def _record_judge_task_outcome(status, scope):
16-
JUDGE_TASK_OUTCOME_TOTAL.labels(status=status, scope=scope).inc()
16+
record_judge_task_outcome(status, scope)
1717

1818

1919
@celery.shared_task(**CELERY_TASK_ARGS())

backend/judge/tests.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,29 @@
77

88
class JudgeTaskObservabilityTest(SimpleTestCase):
99

10-
@mock.patch("judge.tasks.JUDGE_TASK_OUTCOME_TOTAL")
10+
@mock.patch("judge.tasks.record_judge_task_outcome")
1111
@mock.patch("judge.tasks.JudgeDispatcher")
1212
@mock.patch("judge.tasks.User")
1313
@mock.patch("judge.tasks.Submission")
14-
def test_judge_task_records_success_outcome(self, submission_model, user_model, dispatcher, outcome_total):
14+
def test_judge_task_records_success_outcome(self, submission_model, user_model, dispatcher, record_outcome):
1515
submission = mock.Mock(user_id=1, contest_id=None)
1616
submission_model.objects.get.return_value = submission
1717
user_model.objects.get.return_value = mock.Mock(is_disabled=False)
18-
labels = mock.Mock()
19-
outcome_total.labels.return_value = labels
20-
2118
judge_task.run(10, 20)
2219

2320
dispatcher.assert_called_once_with(10, 20)
2421
dispatcher.return_value.judge.assert_called_once()
25-
outcome_total.labels.assert_called_once_with(status="success", scope="practice")
26-
labels.inc.assert_called_once()
22+
record_outcome.assert_called_once_with("success", "practice")
2723

28-
@mock.patch("judge.tasks.JUDGE_TASK_OUTCOME_TOTAL")
24+
@mock.patch("judge.tasks.record_judge_task_outcome")
2925
@mock.patch("judge.tasks.JudgeDispatcher")
3026
@mock.patch("judge.tasks.User")
3127
@mock.patch("judge.tasks.Submission")
32-
def test_judge_task_records_disabled_user_outcome(self, submission_model, user_model, dispatcher, outcome_total):
28+
def test_judge_task_records_disabled_user_outcome(self, submission_model, user_model, dispatcher, record_outcome):
3329
submission = mock.Mock(user_id=1, contest_id=7)
3430
submission_model.objects.get.return_value = submission
3531
user_model.objects.get.return_value = mock.Mock(is_disabled=True)
36-
labels = mock.Mock()
37-
outcome_total.labels.return_value = labels
38-
3932
judge_task.run(10, 20)
4033

4134
dispatcher.assert_not_called()
42-
outcome_total.labels.assert_called_once_with(status="user_disabled", scope="contest")
43-
labels.inc.assert_called_once()
35+
record_outcome.assert_called_once_with("user_disabled", "contest")

backend/oj/celery.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
22
import celery
3+
from celery.signals import task_postrun, task_prerun
34

5+
from utils.observability_context import reset_request_id, set_request_id
46
from utils.observability_tracing import configure_opentelemetry
57

68
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oj.settings')
@@ -9,3 +11,20 @@
911
app = celery.Celery('scheduler')
1012
app.config_from_object('django.conf:settings', namespace='CELERY')
1113
app.autodiscover_tasks()
14+
15+
_request_id_tokens = {}
16+
17+
18+
@task_prerun.connect
19+
def bind_request_id(task_id=None, task=None, **kwargs):
20+
headers = getattr(getattr(task, "request", None), "headers", None) or {}
21+
request_id = headers.get("x-request-id")
22+
if task_id and request_id:
23+
_request_id_tokens[task_id] = set_request_id(request_id)
24+
25+
26+
@task_postrun.connect
27+
def unbind_request_id(task_id=None, **kwargs):
28+
token = _request_id_tokens.pop(task_id, None)
29+
if token is not None:
30+
reset_request_id(token)

0 commit comments

Comments
 (0)