jira/apps/api/plane/celery.py

119 lines
4.5 KiB
Python
Raw Permalink Normal View History

# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Python imports
import os
import logging
[WEB-7447] feat: migrate CE telemetry from OTLP traces to OTLP metrics (#9156) * [WEB-7447] feat: migrate CE telemetry from OTLP traces to OTLP metrics Replace span-based tracing (tracer.py) with OTLP observable gauges, mirroring the approach already used in plane-ee. Key changes: - Add otlp_endpoints.py — shared gRPC/HTTP endpoint helpers - Add telemetry_metrics.py — push_instance_metrics task using MeterProvider + observable gauges (service name: plane-ce-api) - User count excludes bots (is_bot=False) - Page count excludes bot-owned private pages only - Domain derived from WEB_URL env var - Celery beat entry replaced with timedelta schedule + configurable METRICS_PUSH_INTERVAL_MINUTES (default 360 min) - Add explicit opentelemetry-exporter-otlp-proto-grpc dep - Delete tracer.py and telemetry.py (no longer needed) Co-authored-by: Plane AI <noreply@plane.so> * fix: address review comments on CE telemetry metrics - harden grpc_endpoint_from_url for scheme-less OTLP_ENDPOINT values (e.g. "telemetry.plane.so:4317") by prepending "//" before urlparse - fix WEB_URL domain extraction for scheme-less values with same approach - replace N+1 workspace count queries (6×N) with 6 batched annotate(Count) aggregation queries — reduces DB load significantly at WORKSPACE_METRICS_LIMIT - add deterministic ordering (order_by created_at) to workspace slice - harden METRICS_PUSH_INTERVAL_MINUTES env parsing with try/except guard and positive-value validation to avoid crash on malformed input Co-authored-by: Plane AI <noreply@plane.so> * fix: cap METRICS_PUSH_INTERVAL_MINUTES to prevent timedelta overflow Add upper-bound check (10_000_000 minutes) and catch OverflowError alongside ValueError so an arbitrarily large env value cannot crash worker startup via timedelta(minutes=...) OverflowError. Co-authored-by: Plane AI <noreply@plane.so> --------- Co-authored-by: Plane AI <noreply@plane.so>
2026-05-28 13:04:27 +00:00
from datetime import timedelta
# Third party imports
from celery import Celery
fix: Add docker pytest runner and fix bugs the suite surfaced (#9138) * chore(api): add docker compose test runner Adds docker-compose-test.yml at the repo root that boots an isolated postgres / valkey / rabbitmq / minio stack with health checks and tmpfs data dirs, then runs pytest against it and exits. Includes a usage doc under apps/api/tests/RUNNING_TESTS.md and a pointer in AGENTS.md. Prereq: ./setup.sh (generates apps/api/.env). Usage: docker compose -f docker-compose-test.yml up --build \ --abort-on-container-exit --exit-code-from api-tests docker compose -f docker-compose-test.yml down -v * fix(api): correct bugs surfaced by the pytest suite Five small bugs caught by enabling the pytest contract suite end-to-end. Each is independently justifiable: - api/serializers/cycle.py + api/views/cycle.py: CycleCreateSerializer.validate required project_id in the request body, but the view only ever passes it through the URL kwarg. Cycle create/update via the public API was returning 400 "Project ID is required". Read project_id from serializer context (passed by the view) in addition to body/instance. - app/views/api.py: ApiTokenEndpoint.get(pk) and patch(pk) did not filter out is_service=True tokens, so a user could read and modify service tokens through the user token endpoint. The list mode and delete already filter is_service=False; aligned the other two. - bgtasks/work_item_link_task.py: validate_url_ip checked hostname before scheme, so file:///etc/passwd raised "No hostname found" instead of the documented "Only HTTP and HTTPS" error. Swapped the order so the scheme guard matches the docstring intent. - utils/path_validator.py: get_allowed_hosts used `WEB_URL or APP_BASE_URL` so when both are configured to different hosts (the standard local setup: WEB_URL=:8000, APP_BASE_URL=:3000), only one was added to the allow-list. Redirects to APP_BASE_URL then had their next_path stripped because the host wasn't allowed. Include every configured base URL. * chore(api): align pytest tests with current behavior, clear warnings Test-side fixes paired with the product fixes in the previous commit, plus deprecation cleanup that drops the test run from 104 warnings to 0. Tests: - tests/contract/api/test_cycles.py: project fixture sets cycle_view=True; the Project model defaults the flag to False, so cycle create/update always tripped "Cycles are not enabled for this project". - tests/contract/app/test_authentication.py: next_path uses "/workspaces" (validate_next_path rejects values without a leading slash and returns empty, which dropped the path from the redirect URL). - tests/unit/bg_tasks/test_copy_s3_objects.py: mocked sync_with_external_service now returns description_json; the task unconditionally writes the value back to the Issue, and Issue.description_json is NOT NULL on UPDATE. - tests/unit/utils/test_url.py: three length-limit tests placed the URL at char 970+ on a single line, which contains_url truncates away as ReDoS defense (500-char per-line cap). Restructured to keep test intent intact while staying inside the per-line window. Warning cleanup (104 → 0): - settings/common.py: removed USE_L10N=True (deprecated in Django 4.0, removed in 5.0; default is True). - celery.py, settings/local.py, settings/production.py: pythonjsonlogger moved jsonlogger → json; update the import / formatter path.
2026-05-25 19:51:37 +00:00
from pythonjsonlogger.json import JsonFormatter
from celery.signals import after_setup_logger, after_setup_task_logger
[WEB-7447] feat: migrate CE telemetry from OTLP traces to OTLP metrics (#9156) * [WEB-7447] feat: migrate CE telemetry from OTLP traces to OTLP metrics Replace span-based tracing (tracer.py) with OTLP observable gauges, mirroring the approach already used in plane-ee. Key changes: - Add otlp_endpoints.py — shared gRPC/HTTP endpoint helpers - Add telemetry_metrics.py — push_instance_metrics task using MeterProvider + observable gauges (service name: plane-ce-api) - User count excludes bots (is_bot=False) - Page count excludes bot-owned private pages only - Domain derived from WEB_URL env var - Celery beat entry replaced with timedelta schedule + configurable METRICS_PUSH_INTERVAL_MINUTES (default 360 min) - Add explicit opentelemetry-exporter-otlp-proto-grpc dep - Delete tracer.py and telemetry.py (no longer needed) Co-authored-by: Plane AI <noreply@plane.so> * fix: address review comments on CE telemetry metrics - harden grpc_endpoint_from_url for scheme-less OTLP_ENDPOINT values (e.g. "telemetry.plane.so:4317") by prepending "//" before urlparse - fix WEB_URL domain extraction for scheme-less values with same approach - replace N+1 workspace count queries (6×N) with 6 batched annotate(Count) aggregation queries — reduces DB load significantly at WORKSPACE_METRICS_LIMIT - add deterministic ordering (order_by created_at) to workspace slice - harden METRICS_PUSH_INTERVAL_MINUTES env parsing with try/except guard and positive-value validation to avoid crash on malformed input Co-authored-by: Plane AI <noreply@plane.so> * fix: cap METRICS_PUSH_INTERVAL_MINUTES to prevent timedelta overflow Add upper-bound check (10_000_000 minutes) and catch OverflowError alongside ValueError so an arbitrarily large env value cannot crash worker startup via timedelta(minutes=...) OverflowError. Co-authored-by: Plane AI <noreply@plane.so> --------- Co-authored-by: Plane AI <noreply@plane.so>
2026-05-28 13:04:27 +00:00
from celery.schedules import crontab, schedule
# Module imports
from plane.settings.redis import redis_instance
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
ri = redis_instance()
[WEB-7447] feat: migrate CE telemetry from OTLP traces to OTLP metrics (#9156) * [WEB-7447] feat: migrate CE telemetry from OTLP traces to OTLP metrics Replace span-based tracing (tracer.py) with OTLP observable gauges, mirroring the approach already used in plane-ee. Key changes: - Add otlp_endpoints.py — shared gRPC/HTTP endpoint helpers - Add telemetry_metrics.py — push_instance_metrics task using MeterProvider + observable gauges (service name: plane-ce-api) - User count excludes bots (is_bot=False) - Page count excludes bot-owned private pages only - Domain derived from WEB_URL env var - Celery beat entry replaced with timedelta schedule + configurable METRICS_PUSH_INTERVAL_MINUTES (default 360 min) - Add explicit opentelemetry-exporter-otlp-proto-grpc dep - Delete tracer.py and telemetry.py (no longer needed) Co-authored-by: Plane AI <noreply@plane.so> * fix: address review comments on CE telemetry metrics - harden grpc_endpoint_from_url for scheme-less OTLP_ENDPOINT values (e.g. "telemetry.plane.so:4317") by prepending "//" before urlparse - fix WEB_URL domain extraction for scheme-less values with same approach - replace N+1 workspace count queries (6×N) with 6 batched annotate(Count) aggregation queries — reduces DB load significantly at WORKSPACE_METRICS_LIMIT - add deterministic ordering (order_by created_at) to workspace slice - harden METRICS_PUSH_INTERVAL_MINUTES env parsing with try/except guard and positive-value validation to avoid crash on malformed input Co-authored-by: Plane AI <noreply@plane.so> * fix: cap METRICS_PUSH_INTERVAL_MINUTES to prevent timedelta overflow Add upper-bound check (10_000_000 minutes) and catch OverflowError alongside ValueError so an arbitrarily large env value cannot crash worker startup via timedelta(minutes=...) OverflowError. Co-authored-by: Plane AI <noreply@plane.so> --------- Co-authored-by: Plane AI <noreply@plane.so>
2026-05-28 13:04:27 +00:00
# Configurable metrics push interval (in minutes)
# Default: 360 (6 hours), set to 5 for development/testing
def _get_metrics_push_interval_minutes() -> int:
raw = os.environ.get("METRICS_PUSH_INTERVAL_MINUTES", "360")
try:
value = int(raw)
# Cap at 10,000,000 minutes to prevent timedelta(minutes=...) OverflowError
# on arbitrarily large inputs while still allowing multi-year intervals.
return value if 0 < value <= 10_000_000 else 360
except (ValueError, OverflowError):
return 360
METRICS_PUSH_INTERVAL_MINUTES = _get_metrics_push_interval_minutes()
app = Celery("plane")
# Using a string here means the worker will not have to
# pickle the object when using Windows.
app.config_from_object("django.conf:settings", namespace="CELERY")
app.conf.beat_schedule = {
# Intra day recurring jobs
"check-every-five-minutes-to-send-email-notifications": {
"task": "plane.bgtasks.email_notification_task.stack_email_notification",
"schedule": crontab(minute="*/5"), # Every 5 minutes
},
[WEB-7447] feat: migrate CE telemetry from OTLP traces to OTLP metrics (#9156) * [WEB-7447] feat: migrate CE telemetry from OTLP traces to OTLP metrics Replace span-based tracing (tracer.py) with OTLP observable gauges, mirroring the approach already used in plane-ee. Key changes: - Add otlp_endpoints.py — shared gRPC/HTTP endpoint helpers - Add telemetry_metrics.py — push_instance_metrics task using MeterProvider + observable gauges (service name: plane-ce-api) - User count excludes bots (is_bot=False) - Page count excludes bot-owned private pages only - Domain derived from WEB_URL env var - Celery beat entry replaced with timedelta schedule + configurable METRICS_PUSH_INTERVAL_MINUTES (default 360 min) - Add explicit opentelemetry-exporter-otlp-proto-grpc dep - Delete tracer.py and telemetry.py (no longer needed) Co-authored-by: Plane AI <noreply@plane.so> * fix: address review comments on CE telemetry metrics - harden grpc_endpoint_from_url for scheme-less OTLP_ENDPOINT values (e.g. "telemetry.plane.so:4317") by prepending "//" before urlparse - fix WEB_URL domain extraction for scheme-less values with same approach - replace N+1 workspace count queries (6×N) with 6 batched annotate(Count) aggregation queries — reduces DB load significantly at WORKSPACE_METRICS_LIMIT - add deterministic ordering (order_by created_at) to workspace slice - harden METRICS_PUSH_INTERVAL_MINUTES env parsing with try/except guard and positive-value validation to avoid crash on malformed input Co-authored-by: Plane AI <noreply@plane.so> * fix: cap METRICS_PUSH_INTERVAL_MINUTES to prevent timedelta overflow Add upper-bound check (10_000_000 minutes) and catch OverflowError alongside ValueError so an arbitrarily large env value cannot crash worker startup via timedelta(minutes=...) OverflowError. Co-authored-by: Plane AI <noreply@plane.so> --------- Co-authored-by: Plane AI <noreply@plane.so>
2026-05-28 13:04:27 +00:00
"push-instance-metrics": {
"task": "plane.license.bgtasks.telemetry_metrics.push_instance_metrics",
"schedule": schedule(run_every=timedelta(minutes=METRICS_PUSH_INTERVAL_MINUTES)),
},
# Occurs once every day
"check-every-day-to-delete-hard-delete": {
"task": "plane.bgtasks.deletion_task.hard_delete",
"schedule": crontab(hour=0, minute=0), # UTC 00:00
},
"check-every-day-to-archive-and-close": {
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
"schedule": crontab(hour=1, minute=0), # UTC 01:00
},
"check-every-day-to-delete_exporter_history": {
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
"schedule": crontab(hour=1, minute=30), # UTC 01:30
},
2023-11-20 09:23:06 +00:00
"check-every-day-to-delete-file-asset": {
[WEB-310] dev: private bucket implementation (#5793) * chore: migrations and backmigration to move attachments to file asset * chore: move attachments to file assets * chore: update migration file to include created by and updated by and size * chore: remove uninmport errors * chore: make size as float field * fix: file asset uploads * chore: asset uploads migration changes * chore: v2 assets endpoint * chore: remove unused imports * chore: issue attachments * chore: issue attachments * chore: workspace logo endpoints * chore: private bucket changes * chore: user asset endpoint * chore: add logo_url validation * chore: cover image urlk * chore: change asset max length * chore: pages endpoint * chore: store the storage_metadata only when none * chore: attachment asset apis * chore: update create private bucket * chore: make bucket private * chore: fix response of user uploads * fix: response of user uploads * fix: job to fix file asset uploads * fix: user asset endpoints * chore: avatar for user profile * chore: external apis user url endpoint * chore: upload workspace and user asset actions updated * chore: analytics endpoint * fix: analytics export * chore: avatar urls * chore: update user avatar instances * chore: avatar urls for assignees and creators * chore: bucket permission script * fix: all user avatr instances in the web app * chore: update project cover image logic * fix: issue attachment endpoint * chore: patch endpoint for issue attachment * chore: attachments * chore: change attachment storage class * chore: update issue attachment endpoints * fix: issue attachment * chore: update issue attachment implementation * chore: page asset endpoints * fix: web build errors * chore: attachments * chore: page asset urls * chore: comment and issue asset endpoints * chore: asset endpoints * chore: attachment endpoints * chore: bulk asset endpoint * chore: restore endpoint * chore: project assets endpoints * chore: asset url * chore: add delete asset endpoints * chore: fix asset upload endpoint * chore: update patch endpoints * chore: update patch endpoint * chore: update editor image handling * chore: asset restore endpoints * chore: avatar url for space assets * chore: space app assets migration * fix: space app urls * chore: space endpoints * fix: old editor images rendering logic * fix: issue archive and attachment activity * chore: asset deletes * chore: attachment delete * fix: issue attachment * fix: issue attachment get * chore: cover image url for projects * chore: remove duplicate py file * fix: url check function * chore: chore project cover asset delete * fix: migrations * chore: delete migration files * chore: update bucket * fix: build errors * chore: add asset url in intake attachment * chore: project cover fix * chore: update next.config * chore: delete old workspace logos * chore: workspace assets * chore: asset get for space * chore: update project modal * chore: remove unused imports * fix: space app editor helper * chore: update rich-text read-only editor * chore: create multiple column for entity identifiers * chore: update migrations * chore: remove entity identifier * fix: issue assets * chore: update maximum file size logic * chore: update editor max file size logic * fix: close modal after removing workspace logo * chore: update uploaded asstes' status post issue creation * chore: added file size limit to the space app * dev: add file size limit restriction on all endpoints * fix: remove old workspace logo and user avatar --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2024-10-11 14:43:38 +00:00
"task": "plane.bgtasks.file_asset_task.delete_unuploaded_file_asset",
"schedule": crontab(hour=2, minute=0), # UTC 02:00
},
"check-every-day-to-delete-api-logs": {
"task": "plane.bgtasks.cleanup_task.delete_api_logs",
"schedule": crontab(hour=2, minute=30), # UTC 02:30
2024-10-10 09:27:42 +00:00
},
"check-every-day-to-delete-email-notification-logs": {
"task": "plane.bgtasks.cleanup_task.delete_email_notification_logs",
"schedule": crontab(hour=2, minute=45), # UTC 02:45
},
"check-every-day-to-delete-page-versions": {
"task": "plane.bgtasks.cleanup_task.delete_page_versions",
"schedule": crontab(hour=3, minute=0), # UTC 03:00
},
"check-every-day-to-delete-issue-description-versions": {
"task": "plane.bgtasks.cleanup_task.delete_issue_description_versions",
"schedule": crontab(hour=3, minute=15), # UTC 03:15
},
"check-every-day-to-delete-webhook-logs": {
"task": "plane.bgtasks.cleanup_task.delete_webhook_logs",
"schedule": crontab(hour=3, minute=30), # UTC 03:30
},
"check-every-day-to-delete-exporter-history": {
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
"schedule": crontab(hour=3, minute=45), # UTC 03:45
},
}
# Setup logging
@after_setup_logger.connect
def setup_loggers(logger, *args, **kwargs):
formatter = JsonFormatter('"%(levelname)s %(asctime)s %(module)s %(name)s %(message)s')
handler = logging.StreamHandler()
handler.setFormatter(fmt=formatter)
logger.addHandler(handler)
@after_setup_task_logger.connect
def setup_task_loggers(logger, *args, **kwargs):
formatter = JsonFormatter('"%(levelname)s %(asctime)s %(module)s %(name)s %(message)s')
handler = logging.StreamHandler()
handler.setFormatter(fmt=formatter)
logger.addHandler(handler)
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
app.conf.beat_scheduler = "django_celery_beat.schedulers.DatabaseScheduler"