-
Notifications
You must be signed in to change notification settings - Fork 164
feat(BA-3726): Implement ErrorLog Service, Repository Layer
#7803
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
Changes from all commits
259a497
348bd8a
9d42dd9
5f5e952
874773c
829a420
4477fd5
7b5e141
b8fc2ac
372aa0a
1f0ebfb
1643ea3
7680e50
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Implement `ErrorLog` Service, Repository Layer | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| from .types import ErrorLogContent, ErrorLogData, ErrorLogMeta, ErrorLogSeverity | ||
|
|
||
| __all__ = ( | ||
| "ErrorLogContent", | ||
| "ErrorLogData", | ||
| "ErrorLogMeta", | ||
| "ErrorLogSeverity", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import enum | ||
| import uuid | ||
| from dataclasses import dataclass | ||
| from datetime import datetime | ||
| from typing import Any | ||
|
|
||
|
|
||
| class ErrorLogSeverity(enum.StrEnum): | ||
| CRITICAL = "critical" | ||
| ERROR = "error" | ||
| WARNING = "warning" | ||
|
|
||
|
|
||
| @dataclass | ||
| class ErrorLogMeta: | ||
| created_at: datetime | ||
| user: uuid.UUID | None | ||
| source: str | ||
| is_read: bool | ||
| is_cleared: bool | ||
| context_lang: str | ||
| context_env: dict[str, Any] | ||
| request_url: str | None | ||
| request_status: int | None | ||
|
|
||
|
|
||
| @dataclass | ||
| class ErrorLogContent: | ||
| severity: ErrorLogSeverity | ||
| message: str | ||
| traceback: str | None | ||
|
|
||
|
|
||
| @dataclass | ||
| class ErrorLogData: | ||
| id: uuid.UUID | ||
| meta: ErrorLogMeta | ||
| content: ErrorLogContent | ||
|
Comment on lines
+36
to
+40
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's in the code. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,29 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import uuid | ||
| from datetime import datetime | ||
| from typing import Any | ||
|
|
||
| import sqlalchemy as sa | ||
| from sqlalchemy.dialects import postgresql | ||
|
|
||
| from .base import GUID, IDColumn, metadata | ||
| from ai.backend.manager.data.error_log.types import ( | ||
| ErrorLogContent, | ||
| ErrorLogData, | ||
| ErrorLogMeta, | ||
| ErrorLogSeverity, | ||
| ) | ||
|
|
||
| from .base import GUID, Base, IDColumn, mapper_registry | ||
|
|
||
| __all__ = [ | ||
| "error_logs", | ||
| "ErrorLogRow", | ||
| ] | ||
|
|
||
| error_logs = sa.Table( | ||
| "error_logs", | ||
| metadata, | ||
| mapper_registry.metadata, | ||
| IDColumn(), | ||
| sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), index=True), | ||
| sa.Column( | ||
|
|
@@ -26,3 +40,57 @@ | |
| sa.Column("request_status", sa.Integer, nullable=True), | ||
| sa.Column("traceback", sa.Text, nullable=True), | ||
| ) | ||
|
|
||
|
|
||
| class ErrorLogRow(Base): | ||
| __table__ = error_logs | ||
|
|
||
| def __init__( | ||
| self, | ||
| severity: ErrorLogSeverity, | ||
| source: str, | ||
| message: str, | ||
| context_lang: str, | ||
| context_env: dict[str, Any], | ||
| user: uuid.UUID | None = None, | ||
| is_read: bool = False, | ||
| is_cleared: bool = False, | ||
| request_url: str | None = None, | ||
| request_status: int | None = None, | ||
| traceback: str | None = None, | ||
| created_at: datetime | None = None, | ||
| ) -> None: | ||
| self.severity = severity.value | ||
| self.source = source | ||
| self.user = user | ||
| self.is_read = is_read | ||
| self.is_cleared = is_cleared | ||
| self.message = message | ||
| self.context_lang = context_lang | ||
| self.context_env = context_env | ||
| self.request_url = request_url | ||
| self.request_status = request_status | ||
| self.traceback = traceback | ||
| if created_at: | ||
| self.created_at = created_at | ||
|
Comment on lines
+46
to
+75
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove the table code and migrate to SQLAlchemy 2.0. Let's address this in a follow-up. |
||
|
|
||
| def to_dataclass(self) -> ErrorLogData: | ||
| return ErrorLogData( | ||
| id=self.id, | ||
| meta=ErrorLogMeta( | ||
| created_at=self.created_at, | ||
| user=self.user, | ||
| source=self.source, | ||
| is_read=self.is_read, | ||
| is_cleared=self.is_cleared, | ||
| context_lang=self.context_lang, | ||
| context_env=self.context_env, | ||
| request_url=self.request_url, | ||
| request_status=self.request_status, | ||
| ), | ||
| content=ErrorLogContent( | ||
| severity=ErrorLogSeverity(self.severity), | ||
| message=self.message, | ||
| traceback=self.traceback, | ||
| ), | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| from .creators import ErrorLogCreatorSpec | ||
| from .repositories import ErrorLogRepositories | ||
| from .repository import ErrorLogRepository | ||
|
|
||
| __all__ = ( | ||
| "ErrorLogCreatorSpec", | ||
| "ErrorLogRepositories", | ||
| "ErrorLogRepository", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import uuid | ||
| from dataclasses import dataclass | ||
| from datetime import datetime | ||
| from typing import Any, override | ||
|
|
||
| from ai.backend.manager.data.error_log.types import ErrorLogSeverity | ||
| from ai.backend.manager.models.error_logs import ErrorLogRow | ||
| from ai.backend.manager.repositories.base import CreatorSpec | ||
|
|
||
| __all__ = ("ErrorLogCreatorSpec",) | ||
|
|
||
|
|
||
| @dataclass | ||
| class ErrorLogCreatorSpec(CreatorSpec[ErrorLogRow]): | ||
| severity: ErrorLogSeverity | ||
| source: str | ||
| message: str | ||
| context_lang: str | ||
| context_env: dict[str, Any] | ||
| user: uuid.UUID | None = None | ||
| is_read: bool = False | ||
| is_cleared: bool = False | ||
| request_url: str | None = None | ||
| request_status: int | None = None | ||
| traceback: str | None = None | ||
| created_at: datetime | None = None | ||
|
|
||
| @override | ||
| def build_row(self) -> ErrorLogRow: | ||
| return ErrorLogRow( | ||
| severity=self.severity, | ||
| source=self.source, | ||
| message=self.message, | ||
| context_lang=self.context_lang, | ||
| context_env=self.context_env, | ||
| user=self.user, | ||
| is_read=self.is_read, | ||
| is_cleared=self.is_cleared, | ||
| request_url=self.request_url, | ||
| request_status=self.request_status, | ||
| traceback=self.traceback, | ||
| created_at=self.created_at, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from .db_source import ErrorLogDBSource | ||
|
|
||
| __all__ = ("ErrorLogDBSource",) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from ai.backend.common.exception import BackendAIError | ||
| from ai.backend.common.metrics.metric import DomainType, LayerType | ||
| from ai.backend.common.resilience.policies.metrics import MetricArgs, MetricPolicy | ||
| from ai.backend.common.resilience.policies.retry import BackoffStrategy, RetryArgs, RetryPolicy | ||
| from ai.backend.common.resilience.resilience import Resilience | ||
| from ai.backend.manager.data.error_log.types import ErrorLogData | ||
| from ai.backend.manager.models.error_logs import ErrorLogRow | ||
| from ai.backend.manager.repositories.base import ( | ||
| Creator, | ||
| execute_creator, | ||
| ) | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ai.backend.manager.models.utils import ExtendedAsyncSAEngine | ||
|
|
||
| __all__ = ("ErrorLogDBSource",) | ||
|
|
||
| error_log_db_source_resilience = Resilience( | ||
| policies=[ | ||
| MetricPolicy(MetricArgs(domain=DomainType.DB_SOURCE, layer=LayerType.ERROR_LOG_DB_SOURCE)), | ||
| RetryPolicy( | ||
| RetryArgs( | ||
| max_retries=5, | ||
| retry_delay=0.1, | ||
| backoff_strategy=BackoffStrategy.FIXED, | ||
| non_retryable_exceptions=(BackendAIError,), | ||
| ) | ||
| ), | ||
| ] | ||
| ) | ||
|
|
||
|
|
||
| class ErrorLogDBSource: | ||
| _db: ExtendedAsyncSAEngine | ||
|
|
||
| def __init__(self, db: ExtendedAsyncSAEngine) -> None: | ||
| self._db = db | ||
|
|
||
| @error_log_db_source_resilience.apply() | ||
| async def create(self, creator: Creator[ErrorLogRow]) -> ErrorLogData: | ||
| async with self._db.begin_session() as db_sess: | ||
| result = await execute_creator(db_sess, creator) | ||
| return result.row.to_dataclass() | ||
|
Comment on lines
+22
to
+47
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass | ||
| from typing import TYPE_CHECKING, Self | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ai.backend.manager.repositories.types import RepositoryArgs | ||
|
|
||
| from .repository import ErrorLogRepository | ||
|
|
||
|
|
||
| @dataclass | ||
| class ErrorLogRepositories: | ||
| repository: ErrorLogRepository | ||
|
|
||
| @classmethod | ||
| def create(cls, args: RepositoryArgs) -> Self: | ||
| return cls( | ||
| repository=ErrorLogRepository(db=args.db), | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from ai.backend.common.exception import BackendAIError | ||
| from ai.backend.common.metrics.metric import DomainType, LayerType | ||
| from ai.backend.common.resilience.policies.metrics import MetricArgs, MetricPolicy | ||
| from ai.backend.common.resilience.policies.retry import BackoffStrategy, RetryArgs, RetryPolicy | ||
| from ai.backend.common.resilience.resilience import Resilience | ||
| from ai.backend.manager.data.error_log.types import ErrorLogData | ||
| from ai.backend.manager.models.error_logs import ErrorLogRow | ||
| from ai.backend.manager.repositories.base import Creator | ||
|
|
||
| from .db_source import ErrorLogDBSource | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ai.backend.manager.models.utils import ExtendedAsyncSAEngine | ||
|
|
||
| __all__ = ("ErrorLogRepository",) | ||
|
|
||
| error_log_repository_resilience = Resilience( | ||
| policies=[ | ||
| MetricPolicy( | ||
| MetricArgs(domain=DomainType.REPOSITORY, layer=LayerType.ERROR_LOG_REPOSITORY) | ||
| ), | ||
| RetryPolicy( | ||
| RetryArgs( | ||
| max_retries=10, | ||
| retry_delay=0.1, | ||
| backoff_strategy=BackoffStrategy.FIXED, | ||
| non_retryable_exceptions=(BackendAIError,), | ||
| ) | ||
| ), | ||
| ] | ||
| ) | ||
|
|
||
|
|
||
| class ErrorLogRepository: | ||
| _db_source: ErrorLogDBSource | ||
|
|
||
| def __init__(self, db: ExtendedAsyncSAEngine) -> None: | ||
| self._db_source = ErrorLogDBSource(db) | ||
|
|
||
| @error_log_repository_resilience.apply() | ||
| async def create(self, creator: Creator[ErrorLogRow]) -> ErrorLogData: | ||
| return await self._db_source.create(creator) | ||
jopemachine marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from .actions import CreateErrorLogAction, CreateErrorLogActionResult | ||
| from .processors import ErrorLogProcessors | ||
| from .service import ErrorLogService | ||
|
|
||
| __all__ = ( | ||
| "CreateErrorLogAction", | ||
| "CreateErrorLogActionResult", | ||
| "ErrorLogProcessors", | ||
| "ErrorLogService", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| from .base import ErrorLogAction | ||
| from .create import CreateErrorLogAction, CreateErrorLogActionResult | ||
|
|
||
| __all__ = ( | ||
| "ErrorLogAction", | ||
| "CreateErrorLogAction", | ||
| "CreateErrorLogActionResult", | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changelog filename is '7803.feature.md' but the PR description mentions resolving issue #7754. Verify that the changelog filename matches the correct issue number. If #7754 is the correct issue, the file should be renamed to '7754.feature.md'.