quantum-framework 0.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +548 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +565 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +79 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +164 -0
- quantum/core/xml_lines.py +81 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +25 -0
- quantum/runtime/action_handler.py +532 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Job Management Service for Quantum Admin
|
|
3
|
+
Provides admin interface for managing Quantum jobs, schedules, and queues
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, List, Optional, Any
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from dataclasses import asdict
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# quantum is an installed package now (FRAMEWORK_PLAN Fase 1.1). This used to
|
|
15
|
+
# push <repo>/src onto sys.path and import `runtime.job_executor` — the
|
|
16
|
+
# pre-packaging layout. src/ became quantum/, so the import failed, the
|
|
17
|
+
# except swallowed it into a warning, HAS_JOB_EXECUTOR went False, and the
|
|
18
|
+
# admin's whole Jobs subsystem quietly reported zero jobs forever. The repo
|
|
19
|
+
# root is still added so the admin works from a checkout without installing.
|
|
20
|
+
QUANTUM_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
21
|
+
if QUANTUM_ROOT not in sys.path:
|
|
22
|
+
sys.path.insert(0, QUANTUM_ROOT)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from quantum.runtime.job_executor import (
|
|
26
|
+
JobExecutor, JobQueueService, ScheduleService, ThreadService
|
|
27
|
+
)
|
|
28
|
+
HAS_JOB_EXECUTOR = True
|
|
29
|
+
except ImportError as e:
|
|
30
|
+
logger.warning(f"JobExecutor not available: {e}")
|
|
31
|
+
HAS_JOB_EXECUTOR = False
|
|
32
|
+
JobExecutor = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class JobAdminService:
|
|
36
|
+
"""
|
|
37
|
+
Admin service for managing Quantum jobs.
|
|
38
|
+
|
|
39
|
+
Provides:
|
|
40
|
+
- Job queue management
|
|
41
|
+
- Schedule management
|
|
42
|
+
- Thread monitoring
|
|
43
|
+
- Queue statistics
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, job_db_path: str = "quantum_jobs.db"):
|
|
47
|
+
"""
|
|
48
|
+
Initialize job admin service.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
job_db_path: Path to job queue database
|
|
52
|
+
"""
|
|
53
|
+
self._executor: Optional[JobExecutor] = None
|
|
54
|
+
self._job_db_path = job_db_path
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def executor(self) -> Optional[JobExecutor]:
|
|
58
|
+
"""Get or create job executor instance."""
|
|
59
|
+
if not HAS_JOB_EXECUTOR:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
if self._executor is None:
|
|
63
|
+
self._executor = JobExecutor(job_db_path=self._job_db_path)
|
|
64
|
+
return self._executor
|
|
65
|
+
|
|
66
|
+
def is_available(self) -> bool:
|
|
67
|
+
"""Check if job system is available."""
|
|
68
|
+
return HAS_JOB_EXECUTOR
|
|
69
|
+
|
|
70
|
+
# =========================================================================
|
|
71
|
+
# Jobs (Queued)
|
|
72
|
+
# =========================================================================
|
|
73
|
+
|
|
74
|
+
def list_jobs(
|
|
75
|
+
self,
|
|
76
|
+
queue: Optional[str] = None,
|
|
77
|
+
status: Optional[str] = None,
|
|
78
|
+
limit: int = 100
|
|
79
|
+
) -> List[Dict]:
|
|
80
|
+
"""
|
|
81
|
+
List all queued jobs.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
queue: Filter by queue name
|
|
85
|
+
status: Filter by status (pending, running, completed, failed)
|
|
86
|
+
limit: Maximum jobs to return
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
List of job dicts
|
|
90
|
+
"""
|
|
91
|
+
if not self.executor:
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
jobs = self.executor.job_queue.list_jobs(queue=queue, status=status, limit=limit)
|
|
95
|
+
return [self._job_to_dict(job) for job in jobs]
|
|
96
|
+
|
|
97
|
+
def get_job(self, job_id: int) -> Optional[Dict]:
|
|
98
|
+
"""Get job details by ID."""
|
|
99
|
+
if not self.executor:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
job = self.executor.job_queue.get_job(job_id)
|
|
103
|
+
return self._job_to_dict(job) if job else None
|
|
104
|
+
|
|
105
|
+
def cancel_job(self, job_id: int) -> bool:
|
|
106
|
+
"""Cancel a pending job."""
|
|
107
|
+
if not self.executor:
|
|
108
|
+
return False
|
|
109
|
+
return self.executor.job_queue.cancel_job(job_id)
|
|
110
|
+
|
|
111
|
+
def retry_job(self, job_id: int) -> bool:
|
|
112
|
+
"""Retry a failed job."""
|
|
113
|
+
if not self.executor:
|
|
114
|
+
return False
|
|
115
|
+
return self.executor.job_queue.retry_job(job_id)
|
|
116
|
+
|
|
117
|
+
def dispatch_job(
|
|
118
|
+
self,
|
|
119
|
+
name: str,
|
|
120
|
+
queue: str = 'default',
|
|
121
|
+
params: Optional[Dict] = None,
|
|
122
|
+
delay: Optional[str] = None,
|
|
123
|
+
priority: int = 0
|
|
124
|
+
) -> Optional[int]:
|
|
125
|
+
"""
|
|
126
|
+
Dispatch a new job.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
name: Job name
|
|
130
|
+
queue: Queue name
|
|
131
|
+
params: Job parameters
|
|
132
|
+
delay: Delay before execution
|
|
133
|
+
priority: Job priority (0-10)
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Job ID or None
|
|
137
|
+
"""
|
|
138
|
+
if not self.executor:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
return self.executor.job_queue.dispatch(
|
|
142
|
+
name=name,
|
|
143
|
+
queue=queue,
|
|
144
|
+
params=params,
|
|
145
|
+
delay=delay,
|
|
146
|
+
priority=priority
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _job_to_dict(self, job) -> Dict:
|
|
150
|
+
"""Convert JobInfo to dict."""
|
|
151
|
+
if not job:
|
|
152
|
+
return {}
|
|
153
|
+
return {
|
|
154
|
+
'id': job.id,
|
|
155
|
+
'name': job.name,
|
|
156
|
+
'queue': job.queue,
|
|
157
|
+
'params': job.params,
|
|
158
|
+
'status': job.status,
|
|
159
|
+
'attempts': job.attempts,
|
|
160
|
+
'max_attempts': job.max_attempts,
|
|
161
|
+
'scheduled_at': job.scheduled_at.isoformat() if job.scheduled_at else None,
|
|
162
|
+
'started_at': job.started_at.isoformat() if job.started_at else None,
|
|
163
|
+
'completed_at': job.completed_at.isoformat() if job.completed_at else None,
|
|
164
|
+
'error': job.error,
|
|
165
|
+
'created_at': job.created_at.isoformat() if job.created_at else None
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# =========================================================================
|
|
169
|
+
# Schedules
|
|
170
|
+
# =========================================================================
|
|
171
|
+
|
|
172
|
+
def list_schedules(self) -> List[Dict]:
|
|
173
|
+
"""List all scheduled tasks."""
|
|
174
|
+
if not self.executor:
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
schedules = self.executor.schedule.list_schedules()
|
|
178
|
+
return [self._schedule_to_dict(s) for s in schedules]
|
|
179
|
+
|
|
180
|
+
def get_schedule(self, name: str) -> Optional[Dict]:
|
|
181
|
+
"""Get schedule details by name."""
|
|
182
|
+
if not self.executor:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
schedule = self.executor.schedule.get_schedule(name)
|
|
186
|
+
return self._schedule_to_dict(schedule) if schedule else None
|
|
187
|
+
|
|
188
|
+
def pause_schedule(self, name: str) -> bool:
|
|
189
|
+
"""Pause a scheduled task."""
|
|
190
|
+
if not self.executor:
|
|
191
|
+
return False
|
|
192
|
+
return self.executor.schedule.pause_schedule(name)
|
|
193
|
+
|
|
194
|
+
def resume_schedule(self, name: str) -> bool:
|
|
195
|
+
"""Resume a paused schedule."""
|
|
196
|
+
if not self.executor:
|
|
197
|
+
return False
|
|
198
|
+
return self.executor.schedule.resume_schedule(name)
|
|
199
|
+
|
|
200
|
+
def run_schedule_now(self, name: str) -> bool:
|
|
201
|
+
"""Trigger immediate execution of a schedule."""
|
|
202
|
+
if not self.executor:
|
|
203
|
+
return False
|
|
204
|
+
return self.executor.schedule.run_now(name)
|
|
205
|
+
|
|
206
|
+
def remove_schedule(self, name: str) -> bool:
|
|
207
|
+
"""Remove a scheduled task."""
|
|
208
|
+
if not self.executor:
|
|
209
|
+
return False
|
|
210
|
+
return self.executor.schedule.remove_schedule(name)
|
|
211
|
+
|
|
212
|
+
def _schedule_to_dict(self, schedule) -> Dict:
|
|
213
|
+
"""Convert ScheduleInfo to dict."""
|
|
214
|
+
if not schedule:
|
|
215
|
+
return {}
|
|
216
|
+
return {
|
|
217
|
+
'name': schedule.name,
|
|
218
|
+
'trigger_type': schedule.trigger_type,
|
|
219
|
+
'trigger_info': schedule.trigger_info,
|
|
220
|
+
'next_run': schedule.next_run.isoformat() if schedule.next_run else None,
|
|
221
|
+
'enabled': schedule.enabled,
|
|
222
|
+
'run_count': schedule.run_count,
|
|
223
|
+
'last_run': schedule.last_run.isoformat() if schedule.last_run else None,
|
|
224
|
+
'last_error': schedule.last_error
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# =========================================================================
|
|
228
|
+
# Threads
|
|
229
|
+
# =========================================================================
|
|
230
|
+
|
|
231
|
+
def list_threads(self, status: Optional[str] = None) -> List[Dict]:
|
|
232
|
+
"""List all threads."""
|
|
233
|
+
if not self.executor:
|
|
234
|
+
return []
|
|
235
|
+
|
|
236
|
+
threads = self.executor.thread.list_threads(status=status)
|
|
237
|
+
return [self._thread_to_dict(t) for t in threads]
|
|
238
|
+
|
|
239
|
+
def get_thread(self, name: str) -> Optional[Dict]:
|
|
240
|
+
"""Get thread details by name."""
|
|
241
|
+
if not self.executor:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
thread = self.executor.thread.get_thread(name)
|
|
245
|
+
return self._thread_to_dict(thread) if thread else None
|
|
246
|
+
|
|
247
|
+
def terminate_thread(self, name: str) -> bool:
|
|
248
|
+
"""Request thread termination."""
|
|
249
|
+
if not self.executor:
|
|
250
|
+
return False
|
|
251
|
+
return self.executor.thread.terminate_thread(name)
|
|
252
|
+
|
|
253
|
+
def _thread_to_dict(self, thread) -> Dict:
|
|
254
|
+
"""Convert ThreadInfo to dict."""
|
|
255
|
+
if not thread:
|
|
256
|
+
return {}
|
|
257
|
+
return {
|
|
258
|
+
'name': thread.name,
|
|
259
|
+
'priority': thread.priority,
|
|
260
|
+
'started_at': thread.started_at.isoformat() if thread.started_at else None,
|
|
261
|
+
'status': thread.status,
|
|
262
|
+
'timeout': thread.timeout,
|
|
263
|
+
'error': thread.error
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# =========================================================================
|
|
267
|
+
# Queues
|
|
268
|
+
# =========================================================================
|
|
269
|
+
|
|
270
|
+
def list_queues(self) -> List[Dict]:
|
|
271
|
+
"""List all queues with stats."""
|
|
272
|
+
if not self.executor:
|
|
273
|
+
return [{'name': 'default', 'pending': 0, 'running': 0, 'completed': 0, 'failed': 0}]
|
|
274
|
+
return self.executor.list_queues()
|
|
275
|
+
|
|
276
|
+
def get_queue_stats(self, queue: str = 'default') -> Dict:
|
|
277
|
+
"""Get queue statistics."""
|
|
278
|
+
if not self.executor:
|
|
279
|
+
return {'pending': 0, 'running': 0, 'completed': 0, 'failed': 0}
|
|
280
|
+
return self.executor.job_queue.get_queue_stats(queue)
|
|
281
|
+
|
|
282
|
+
def purge_queue(self, queue: str, status: Optional[str] = None) -> int:
|
|
283
|
+
"""Purge jobs from a queue."""
|
|
284
|
+
if not self.executor:
|
|
285
|
+
return 0
|
|
286
|
+
return self.executor.job_queue.purge_queue(queue, status=status)
|
|
287
|
+
|
|
288
|
+
# =========================================================================
|
|
289
|
+
# Overview
|
|
290
|
+
# =========================================================================
|
|
291
|
+
|
|
292
|
+
def get_overview(self) -> Dict:
|
|
293
|
+
"""
|
|
294
|
+
Get job system overview.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Dict with counts and status
|
|
298
|
+
"""
|
|
299
|
+
if not self.executor:
|
|
300
|
+
return {
|
|
301
|
+
'available': False,
|
|
302
|
+
'message': 'Job executor not available'
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Get queue stats
|
|
306
|
+
queue_stats = self.get_queue_stats()
|
|
307
|
+
|
|
308
|
+
# Get schedule count
|
|
309
|
+
schedules = self.list_schedules()
|
|
310
|
+
active_schedules = len([s for s in schedules if s.get('enabled')])
|
|
311
|
+
|
|
312
|
+
# Get thread count
|
|
313
|
+
threads = self.list_threads()
|
|
314
|
+
running_threads = len([t for t in threads if t.get('status') == 'running'])
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
'available': True,
|
|
318
|
+
'jobs': {
|
|
319
|
+
'pending': queue_stats.get('pending', 0),
|
|
320
|
+
'running': queue_stats.get('running', 0),
|
|
321
|
+
'completed': queue_stats.get('completed', 0),
|
|
322
|
+
'failed': queue_stats.get('failed', 0),
|
|
323
|
+
'total': sum(queue_stats.values())
|
|
324
|
+
},
|
|
325
|
+
'schedules': {
|
|
326
|
+
'total': len(schedules),
|
|
327
|
+
'active': active_schedules,
|
|
328
|
+
'paused': len(schedules) - active_schedules
|
|
329
|
+
},
|
|
330
|
+
'threads': {
|
|
331
|
+
'total': len(threads),
|
|
332
|
+
'running': running_threads
|
|
333
|
+
},
|
|
334
|
+
'queues': self.list_queues()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# Singleton instance
|
|
339
|
+
_job_service: Optional[JobAdminService] = None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def get_job_service() -> JobAdminService:
|
|
343
|
+
"""Get job service singleton."""
|
|
344
|
+
global _job_service
|
|
345
|
+
if _job_service is None:
|
|
346
|
+
_job_service = JobAdminService()
|
|
347
|
+
return _job_service
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Log Service for Quantum Admin
|
|
3
|
+
Unified logging for application, database, and access logs
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import asyncio
|
|
8
|
+
from typing import Optional, List, Dict, Any
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from sqlalchemy.orm import Session
|
|
11
|
+
from sqlalchemy import desc, or_
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from .models import ApplicationLog, Project, Environment
|
|
17
|
+
except ImportError:
|
|
18
|
+
from models import ApplicationLog, Project, Environment
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LogLevel:
|
|
22
|
+
DEBUG = "DEBUG"
|
|
23
|
+
INFO = "INFO"
|
|
24
|
+
WARNING = "WARNING"
|
|
25
|
+
ERROR = "ERROR"
|
|
26
|
+
CRITICAL = "CRITICAL"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LogSource:
|
|
30
|
+
APP = "app" # Application logs (stdout/stderr)
|
|
31
|
+
DB = "database" # Database query logs
|
|
32
|
+
ACCESS = "access" # HTTP access logs
|
|
33
|
+
API = "api" # External API calls
|
|
34
|
+
SECURITY = "security" # Security events (auth, permissions)
|
|
35
|
+
DEPLOY = "deploy" # Deployment logs
|
|
36
|
+
SYSTEM = "system" # System/infrastructure logs
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def all_sources(cls):
|
|
40
|
+
return [cls.APP, cls.DB, cls.ACCESS, cls.API, cls.SECURITY, cls.DEPLOY, cls.SYSTEM]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def create_log(
|
|
44
|
+
db: Session,
|
|
45
|
+
project_id: int,
|
|
46
|
+
level: str,
|
|
47
|
+
source: str,
|
|
48
|
+
message: str,
|
|
49
|
+
environment_id: int = None,
|
|
50
|
+
component_name: str = None,
|
|
51
|
+
function_name: str = None,
|
|
52
|
+
line_number: int = None,
|
|
53
|
+
request_id: str = None,
|
|
54
|
+
request_method: str = None,
|
|
55
|
+
request_path: str = None,
|
|
56
|
+
response_status: int = None,
|
|
57
|
+
response_time_ms: int = None,
|
|
58
|
+
extra_data: Dict = None
|
|
59
|
+
) -> ApplicationLog:
|
|
60
|
+
"""
|
|
61
|
+
Create a log entry.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
db: Database session
|
|
65
|
+
project_id: Project this log belongs to
|
|
66
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
67
|
+
source: Log source (app, db, access, deploy, system)
|
|
68
|
+
message: Log message
|
|
69
|
+
environment_id: Optional environment ID
|
|
70
|
+
component_name: Optional component name
|
|
71
|
+
function_name: Optional function name
|
|
72
|
+
line_number: Optional line number
|
|
73
|
+
request_id: Optional request ID for tracing
|
|
74
|
+
request_method: Optional HTTP method
|
|
75
|
+
request_path: Optional request path
|
|
76
|
+
response_status: Optional HTTP status code
|
|
77
|
+
response_time_ms: Optional response time in milliseconds
|
|
78
|
+
extra_data: Optional additional data as dictionary
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The created ApplicationLog object
|
|
82
|
+
"""
|
|
83
|
+
log_entry = ApplicationLog(
|
|
84
|
+
project_id=project_id,
|
|
85
|
+
environment_id=environment_id,
|
|
86
|
+
timestamp=datetime.utcnow(),
|
|
87
|
+
level=level,
|
|
88
|
+
source=source,
|
|
89
|
+
message=message,
|
|
90
|
+
component_name=component_name,
|
|
91
|
+
function_name=function_name,
|
|
92
|
+
line_number=line_number,
|
|
93
|
+
request_id=request_id,
|
|
94
|
+
request_method=request_method,
|
|
95
|
+
request_path=request_path,
|
|
96
|
+
response_status=response_status,
|
|
97
|
+
response_time_ms=response_time_ms,
|
|
98
|
+
extra_data=json.dumps(extra_data) if extra_data else None
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
db.add(log_entry)
|
|
102
|
+
db.commit()
|
|
103
|
+
db.refresh(log_entry)
|
|
104
|
+
|
|
105
|
+
return log_entry
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_logs(
|
|
109
|
+
db: Session,
|
|
110
|
+
project_id: int,
|
|
111
|
+
source: str = None,
|
|
112
|
+
level: str = None,
|
|
113
|
+
environment_id: int = None,
|
|
114
|
+
search: str = None,
|
|
115
|
+
since: datetime = None,
|
|
116
|
+
until: datetime = None,
|
|
117
|
+
limit: int = 100,
|
|
118
|
+
offset: int = 0
|
|
119
|
+
) -> List[ApplicationLog]:
|
|
120
|
+
"""
|
|
121
|
+
Query logs with optional filters.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
db: Database session
|
|
125
|
+
project_id: Filter by project
|
|
126
|
+
source: Filter by source (app, db, access, deploy, system)
|
|
127
|
+
level: Filter by log level
|
|
128
|
+
environment_id: Filter by environment
|
|
129
|
+
search: Search in message
|
|
130
|
+
since: Filter entries after this time
|
|
131
|
+
until: Filter entries before this time
|
|
132
|
+
limit: Maximum number of entries to return
|
|
133
|
+
offset: Number of entries to skip
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List of ApplicationLog objects
|
|
137
|
+
"""
|
|
138
|
+
query = db.query(ApplicationLog).filter(ApplicationLog.project_id == project_id)
|
|
139
|
+
|
|
140
|
+
if source and source != 'all':
|
|
141
|
+
query = query.filter(ApplicationLog.source == source)
|
|
142
|
+
if level:
|
|
143
|
+
query = query.filter(ApplicationLog.level == level)
|
|
144
|
+
if environment_id:
|
|
145
|
+
query = query.filter(ApplicationLog.environment_id == environment_id)
|
|
146
|
+
if search:
|
|
147
|
+
query = query.filter(ApplicationLog.message.ilike(f'%{search}%'))
|
|
148
|
+
if since:
|
|
149
|
+
query = query.filter(ApplicationLog.timestamp >= since)
|
|
150
|
+
if until:
|
|
151
|
+
query = query.filter(ApplicationLog.timestamp <= until)
|
|
152
|
+
|
|
153
|
+
return query.order_by(desc(ApplicationLog.timestamp)).offset(offset).limit(limit).all()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_log_stats(
|
|
157
|
+
db: Session,
|
|
158
|
+
project_id: int,
|
|
159
|
+
hours: int = 24
|
|
160
|
+
) -> Dict:
|
|
161
|
+
"""
|
|
162
|
+
Get log statistics for a project.
|
|
163
|
+
"""
|
|
164
|
+
from sqlalchemy import func
|
|
165
|
+
|
|
166
|
+
since = datetime.utcnow() - timedelta(hours=hours)
|
|
167
|
+
query = db.query(ApplicationLog).filter(
|
|
168
|
+
ApplicationLog.project_id == project_id,
|
|
169
|
+
ApplicationLog.timestamp >= since
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
total = query.count()
|
|
173
|
+
|
|
174
|
+
# Count by level
|
|
175
|
+
level_counts = {}
|
|
176
|
+
for level in [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARNING, LogLevel.ERROR, LogLevel.CRITICAL]:
|
|
177
|
+
count = query.filter(ApplicationLog.level == level).count()
|
|
178
|
+
level_counts[level.lower()] = count
|
|
179
|
+
|
|
180
|
+
# Count by source
|
|
181
|
+
source_counts = {}
|
|
182
|
+
for source in LogSource.all_sources():
|
|
183
|
+
count = query.filter(ApplicationLog.source == source).count()
|
|
184
|
+
source_counts[source] = count
|
|
185
|
+
|
|
186
|
+
# Error rate
|
|
187
|
+
error_count = level_counts.get('error', 0) + level_counts.get('critical', 0)
|
|
188
|
+
error_rate = (error_count / total * 100) if total > 0 else 0
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
"total": total,
|
|
192
|
+
"period_hours": hours,
|
|
193
|
+
"by_level": level_counts,
|
|
194
|
+
"by_source": source_counts,
|
|
195
|
+
"error_rate": round(error_rate, 2)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def format_log_for_display(log: ApplicationLog) -> Dict:
|
|
200
|
+
"""Format a log entry for display."""
|
|
201
|
+
return {
|
|
202
|
+
"id": log.id,
|
|
203
|
+
"timestamp": log.timestamp.strftime('%H:%M:%S') if log.timestamp else '',
|
|
204
|
+
"timestamp_full": log.timestamp.isoformat() if log.timestamp else '',
|
|
205
|
+
"level": log.level,
|
|
206
|
+
"source": log.source,
|
|
207
|
+
"message": log.message[:200] if log.message else '',
|
|
208
|
+
"full_message": log.message,
|
|
209
|
+
"component": log.component_name,
|
|
210
|
+
"function": log.function_name,
|
|
211
|
+
"request": f"{log.request_method} {log.request_path}" if log.request_method else None,
|
|
212
|
+
"status": log.response_status,
|
|
213
|
+
"duration": f"{log.response_time_ms}ms" if log.response_time_ms else None
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class LogService:
|
|
218
|
+
"""Service class for log operations"""
|
|
219
|
+
|
|
220
|
+
def __init__(self, db: Session):
|
|
221
|
+
self.db = db
|
|
222
|
+
|
|
223
|
+
def log(
|
|
224
|
+
self,
|
|
225
|
+
project_id: int,
|
|
226
|
+
level: str,
|
|
227
|
+
source: str,
|
|
228
|
+
message: str,
|
|
229
|
+
**kwargs
|
|
230
|
+
) -> ApplicationLog:
|
|
231
|
+
"""Create a log entry"""
|
|
232
|
+
return create_log(
|
|
233
|
+
self.db,
|
|
234
|
+
project_id=project_id,
|
|
235
|
+
level=level,
|
|
236
|
+
source=source,
|
|
237
|
+
message=message,
|
|
238
|
+
**kwargs
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def info(self, project_id: int, source: str, message: str, **kwargs) -> ApplicationLog:
|
|
242
|
+
return self.log(project_id, LogLevel.INFO, source, message, **kwargs)
|
|
243
|
+
|
|
244
|
+
def warning(self, project_id: int, source: str, message: str, **kwargs) -> ApplicationLog:
|
|
245
|
+
return self.log(project_id, LogLevel.WARNING, source, message, **kwargs)
|
|
246
|
+
|
|
247
|
+
def error(self, project_id: int, source: str, message: str, **kwargs) -> ApplicationLog:
|
|
248
|
+
return self.log(project_id, LogLevel.ERROR, source, message, **kwargs)
|
|
249
|
+
|
|
250
|
+
def debug(self, project_id: int, source: str, message: str, **kwargs) -> ApplicationLog:
|
|
251
|
+
return self.log(project_id, LogLevel.DEBUG, source, message, **kwargs)
|
|
252
|
+
|
|
253
|
+
def access_log(
|
|
254
|
+
self,
|
|
255
|
+
project_id: int,
|
|
256
|
+
method: str,
|
|
257
|
+
path: str,
|
|
258
|
+
status: int,
|
|
259
|
+
duration_ms: int,
|
|
260
|
+
**kwargs
|
|
261
|
+
) -> ApplicationLog:
|
|
262
|
+
"""Create an access log entry"""
|
|
263
|
+
message = f"{method} {path} {status} {duration_ms}ms"
|
|
264
|
+
return self.log(
|
|
265
|
+
project_id,
|
|
266
|
+
LogLevel.INFO if status < 400 else LogLevel.WARNING if status < 500 else LogLevel.ERROR,
|
|
267
|
+
LogSource.ACCESS,
|
|
268
|
+
message,
|
|
269
|
+
request_method=method,
|
|
270
|
+
request_path=path,
|
|
271
|
+
response_status=status,
|
|
272
|
+
response_time_ms=duration_ms,
|
|
273
|
+
**kwargs
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def query_log(
|
|
277
|
+
self,
|
|
278
|
+
project_id: int,
|
|
279
|
+
query: str,
|
|
280
|
+
duration_ms: int,
|
|
281
|
+
**kwargs
|
|
282
|
+
) -> ApplicationLog:
|
|
283
|
+
"""Create a database query log entry"""
|
|
284
|
+
message = f"Query executed in {duration_ms}ms: {query[:100]}..."
|
|
285
|
+
return self.log(
|
|
286
|
+
project_id,
|
|
287
|
+
LogLevel.DEBUG,
|
|
288
|
+
LogSource.DB,
|
|
289
|
+
message,
|
|
290
|
+
response_time_ms=duration_ms,
|
|
291
|
+
**kwargs
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def get_logs(
|
|
295
|
+
self,
|
|
296
|
+
project_id: int,
|
|
297
|
+
source: str = None,
|
|
298
|
+
level: str = None,
|
|
299
|
+
environment_id: int = None,
|
|
300
|
+
search: str = None,
|
|
301
|
+
since: datetime = None,
|
|
302
|
+
until: datetime = None,
|
|
303
|
+
limit: int = 100,
|
|
304
|
+
offset: int = 0
|
|
305
|
+
) -> List[ApplicationLog]:
|
|
306
|
+
"""Query logs"""
|
|
307
|
+
return get_logs(
|
|
308
|
+
self.db,
|
|
309
|
+
project_id=project_id,
|
|
310
|
+
source=source,
|
|
311
|
+
level=level,
|
|
312
|
+
environment_id=environment_id,
|
|
313
|
+
search=search,
|
|
314
|
+
since=since,
|
|
315
|
+
until=until,
|
|
316
|
+
limit=limit,
|
|
317
|
+
offset=offset
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def get_stats(self, project_id: int, hours: int = 24) -> Dict:
|
|
321
|
+
"""Get log statistics"""
|
|
322
|
+
return get_log_stats(self.db, project_id=project_id, hours=hours)
|
|
323
|
+
|
|
324
|
+
def format_logs(self, logs: List[ApplicationLog]) -> List[Dict]:
|
|
325
|
+
"""Format logs for display"""
|
|
326
|
+
return [format_log_for_display(log) for log in logs]
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def get_log_service(db: Session) -> LogService:
|
|
330
|
+
"""Get log service instance"""
|
|
331
|
+
return LogService(db)
|