mirror of
				https://github.com/arcan1s/ahriman.git
				synced 2025-10-31 05:43:41 +00:00 
			
		
		
		
	refine package logging
This commit is contained in:
		| @ -7,6 +7,8 @@ logging = ahriman.ini.d/logging.ini | |||||||
| ;apply_migrations = yes | ;apply_migrations = yes | ||||||
| ; Path to the application SQLite database. | ; Path to the application SQLite database. | ||||||
| database = ${repository:root}/ahriman.db | database = ${repository:root}/ahriman.db | ||||||
|  | ; Keep last build logs for each package | ||||||
|  | keep_last_logs = 5 | ||||||
|  |  | ||||||
| [alpm] | [alpm] | ||||||
| ; Path to pacman system database cache. | ; Path to pacman system database cache. | ||||||
|  | |||||||
| @ -45,6 +45,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = { | |||||||
|                 "path_exists": True, |                 "path_exists": True, | ||||||
|                 "path_type": "dir", |                 "path_type": "dir", | ||||||
|             }, |             }, | ||||||
|  |             "keep_last_logs": { | ||||||
|  |                 "type": "integer", | ||||||
|  |                 "coerce": "integer", | ||||||
|  |                 "min": 0, | ||||||
|  |             }, | ||||||
|             "logging": { |             "logging": { | ||||||
|                 "type": "path", |                 "type": "path", | ||||||
|                 "coerce": "absolute_path", |                 "coerce": "absolute_path", | ||||||
|  | |||||||
							
								
								
									
										27
									
								
								src/ahriman/core/database/migrations/m015_logs_process_id.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/ahriman/core/database/migrations/m015_logs_process_id.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | |||||||
|  | # | ||||||
|  | # Copyright (c) 2021-2025 ahriman team. | ||||||
|  | # | ||||||
|  | # This file is part of ahriman | ||||||
|  | # (see https://github.com/arcan1s/ahriman). | ||||||
|  | # | ||||||
|  | # This program is free software: you can redistribute it and/or modify | ||||||
|  | # it under the terms of the GNU General Public License as published by | ||||||
|  | # the Free Software Foundation, either version 3 of the License, or | ||||||
|  | # (at your option) any later version. | ||||||
|  | # | ||||||
|  | # This program is distributed in the hope that it will be useful, | ||||||
|  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | # GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | # You should have received a copy of the GNU General Public License | ||||||
|  | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | # | ||||||
|  | __all__ = ["steps"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | steps = [ | ||||||
|  |     """ | ||||||
|  |     alter table logs add column process_id text not null default '' | ||||||
|  |     """, | ||||||
|  | ] | ||||||
| @ -30,7 +30,7 @@ class LogsOperations(Operations): | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def logs_get(self, package_base: str, limit: int = -1, offset: int = 0, |     def logs_get(self, package_base: str, limit: int = -1, offset: int = 0, | ||||||
|                  repository_id: RepositoryId | None = None) -> list[tuple[float, str]]: |                  repository_id: RepositoryId | None = None) -> list[tuple[LogRecordId, float, str]]: | ||||||
|         """ |         """ | ||||||
|         extract logs for specified package base |         extract logs for specified package base | ||||||
|  |  | ||||||
| @ -41,16 +41,16 @@ class LogsOperations(Operations): | |||||||
|             repository_id(RepositoryId, optional): repository unique identifier override (Default value = None) |             repository_id(RepositoryId, optional): repository unique identifier override (Default value = None) | ||||||
|  |  | ||||||
|         Return: |         Return: | ||||||
|             list[tuple[float, str]]: sorted package log records and their timestamps |             list[tuple[LogRecordId, float, str]]: sorted package log records and their timestamps | ||||||
|         """ |         """ | ||||||
|         repository_id = repository_id or self._repository_id |         repository_id = repository_id or self._repository_id | ||||||
|  |  | ||||||
|         def run(connection: Connection) -> list[tuple[float, str]]: |         def run(connection: Connection) -> list[tuple[LogRecordId, float, str]]: | ||||||
|             return [ |             return [ | ||||||
|                 (row["created"], row["record"]) |                 (LogRecordId(package_base, row["version"], row["process_id"]), row["created"], row["record"]) | ||||||
|                 for row in connection.execute( |                 for row in connection.execute( | ||||||
|                     """ |                     """ | ||||||
|                     select created, record from ( |                     select created, record, version, process_id from ( | ||||||
|                         select * from logs |                         select * from logs | ||||||
|                         where package_base = :package_base and repository = :repository |                         where package_base = :package_base and repository = :repository | ||||||
|                         order by created desc limit :limit offset :offset |                         order by created desc limit :limit offset :offset | ||||||
| @ -83,9 +83,9 @@ class LogsOperations(Operations): | |||||||
|             connection.execute( |             connection.execute( | ||||||
|                 """ |                 """ | ||||||
|                 insert into logs |                 insert into logs | ||||||
|                 (package_base, version, created, record, repository) |                 (package_base, version, created, record, repository, process_id) | ||||||
|                 values |                 values | ||||||
|                 (:package_base, :version, :created, :record, :repository) |                 (:package_base, :version, :created, :record, :repository, :process_id) | ||||||
|                 """, |                 """, | ||||||
|                 { |                 { | ||||||
|                     "package_base": log_record_id.package_base, |                     "package_base": log_record_id.package_base, | ||||||
| @ -93,6 +93,7 @@ class LogsOperations(Operations): | |||||||
|                     "created": created, |                     "created": created, | ||||||
|                     "record": record, |                     "record": record, | ||||||
|                     "repository": repository_id.id, |                     "repository": repository_id.id, | ||||||
|  |                     "process_id": log_record_id.process_id, | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
| @ -125,3 +126,54 @@ class LogsOperations(Operations): | |||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         return self.with_connection(run, commit=True) |         return self.with_connection(run, commit=True) | ||||||
|  |  | ||||||
|  |     def logs_rotate(self, keep_last_records: int, repository_id: RepositoryId | None = None) -> None: | ||||||
|  |         """ | ||||||
|  |         rotate logs in storage. This method will remove old logs, keeping only the last N records for each package | ||||||
|  |  | ||||||
|  |         Args: | ||||||
|  |             keep_last_records(int): number of last records to keep | ||||||
|  |             repository_id(RepositoryId, optional): repository unique identifier override (Default value = None) | ||||||
|  |         """ | ||||||
|  |         repository_id = repository_id or self._repository_id | ||||||
|  |  | ||||||
|  |         def remove_duplicates(connection: Connection) -> None: | ||||||
|  |             connection.execute( | ||||||
|  |                 """ | ||||||
|  |                 delete from logs | ||||||
|  |                 where (package_base, version, repository, process_id) not in ( | ||||||
|  |                   select package_base, version, repository, process_id from logs | ||||||
|  |                   where (package_base, version, repository, created) in ( | ||||||
|  |                     select package_base, version, repository, max(created) from logs | ||||||
|  |                     where repository = :repository | ||||||
|  |                     group by package_base, version, repository | ||||||
|  |                   ) | ||||||
|  |                 ) | ||||||
|  |                 """, | ||||||
|  |                 { | ||||||
|  |                     "repository": repository_id.id, | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         def remove_older(connection: Connection) -> None: | ||||||
|  |             connection.execute( | ||||||
|  |                 """ | ||||||
|  |                 delete from logs | ||||||
|  |                 where (package_base, repository, process_id) in ( | ||||||
|  |                   select package_base, repository, process_id from logs | ||||||
|  |                   where repository = :repository | ||||||
|  |                   group by package_base, repository, process_id | ||||||
|  |                   order by min(created) desc limit -1 offset :offset | ||||||
|  |                 ) | ||||||
|  |                 """, | ||||||
|  |                 { | ||||||
|  |                     "offset": keep_last_records, | ||||||
|  |                     "repository": repository_id.id, | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         def run(connection: Connection) -> None: | ||||||
|  |             remove_duplicates(connection) | ||||||
|  |             remove_older(connection) | ||||||
|  |  | ||||||
|  |         return self.with_connection(run, commit=True) | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ | |||||||
| # You should have received a copy of the GNU General Public License | # You should have received a copy of the GNU General Public License | ||||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||||
| # | # | ||||||
|  | import atexit | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| from typing import Self | from typing import Self | ||||||
| @ -33,6 +34,7 @@ class HttpLogHandler(logging.Handler): | |||||||
|     method |     method | ||||||
|  |  | ||||||
|     Attributes: |     Attributes: | ||||||
|  |         keep_last_records(int): number of last records to keep | ||||||
|         reporter(Client): build status reporter instance |         reporter(Client): build status reporter instance | ||||||
|         suppress_errors(bool): suppress logging errors (e.g. if no web server available) |         suppress_errors(bool): suppress logging errors (e.g. if no web server available) | ||||||
|     """ |     """ | ||||||
| @ -51,6 +53,7 @@ class HttpLogHandler(logging.Handler): | |||||||
|  |  | ||||||
|         self.reporter = Client.load(repository_id, configuration, report=report) |         self.reporter = Client.load(repository_id, configuration, report=report) | ||||||
|         self.suppress_errors = suppress_errors |         self.suppress_errors = suppress_errors | ||||||
|  |         self.keep_last_records = configuration.getint("settings", "keep_last_logs", fallback=0) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def load(cls, repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Self: |     def load(cls, repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Self: | ||||||
| @ -76,6 +79,8 @@ class HttpLogHandler(logging.Handler): | |||||||
|         handler = cls(repository_id, configuration, report=report, suppress_errors=suppress_errors) |         handler = cls(repository_id, configuration, report=report, suppress_errors=suppress_errors) | ||||||
|         root.addHandler(handler) |         root.addHandler(handler) | ||||||
|  |  | ||||||
|  |         atexit.register(handler.rotate) | ||||||
|  |  | ||||||
|         return handler |         return handler | ||||||
|  |  | ||||||
|     def emit(self, record: logging.LogRecord) -> None: |     def emit(self, record: logging.LogRecord) -> None: | ||||||
| @ -95,3 +100,9 @@ class HttpLogHandler(logging.Handler): | |||||||
|             if self.suppress_errors: |             if self.suppress_errors: | ||||||
|                 return |                 return | ||||||
|             self.handleError(record) |             self.handleError(record) | ||||||
|  |  | ||||||
|  |     def rotate(self) -> None: | ||||||
|  |         """ | ||||||
|  |         rotate log records, removing older ones | ||||||
|  |         """ | ||||||
|  |         self.reporter.logs_rotate(self.keep_last_records) | ||||||
|  | |||||||
| @ -99,24 +99,3 @@ class LazyLogging: | |||||||
|             yield |             yield | ||||||
|         finally: |         finally: | ||||||
|             self._package_logger_reset() |             self._package_logger_reset() | ||||||
|  |  | ||||||
|     @contextlib.contextmanager |  | ||||||
|     def suppress_logging(self, log_level: int = logging.WARNING) -> Generator[None, None, None]: |  | ||||||
|         """ |  | ||||||
|         silence log messages in context |  | ||||||
|  |  | ||||||
|         Args: |  | ||||||
|             log_level(int, optional): the highest log level to keep (Default value = logging.WARNING) |  | ||||||
|  |  | ||||||
|         Examples: |  | ||||||
|              This function is designed to be used to suppress all log messages in context, e.g.: |  | ||||||
|  |  | ||||||
|                  >>> with self.suppress_logging(): |  | ||||||
|                  >>>     do_some_noisy_actions() |  | ||||||
|         """ |  | ||||||
|         current_level = self.logger.manager.disable |  | ||||||
|         try: |  | ||||||
|             logging.disable(log_level) |  | ||||||
|             yield |  | ||||||
|         finally: |  | ||||||
|             logging.disable(current_level) |  | ||||||
|  | |||||||
| @ -75,7 +75,7 @@ class Executor(PackageInfo, Cleaner): | |||||||
|  |  | ||||||
|         result = Result() |         result = Result() | ||||||
|         for single in updates: |         for single in updates: | ||||||
|             with self.in_package_context(single.base, local_versions.get(single.base)), \ |             with self.in_package_context(single.base, single.version), \ | ||||||
|                     TemporaryDirectory(ignore_cleanup_errors=True) as dir_name: |                     TemporaryDirectory(ignore_cleanup_errors=True) as dir_name: | ||||||
|                 try: |                 try: | ||||||
|                     with self.in_event(single.base, EventType.PackageUpdated, failure=EventType.PackageUpdateFailed): |                     with self.in_event(single.base, EventType.PackageUpdated, failure=EventType.PackageUpdateFailed): | ||||||
| @ -194,7 +194,6 @@ class Executor(PackageInfo, Cleaner): | |||||||
|             self.repo.add(package_path) |             self.repo.add(package_path) | ||||||
|  |  | ||||||
|         current_packages = {package.base: package for package in self.packages()} |         current_packages = {package.base: package for package in self.packages()} | ||||||
|         local_versions = {package_base: package.version for package_base, package in current_packages.items()} |  | ||||||
|  |  | ||||||
|         removed_packages: list[str] = []  # list of packages which have been removed from the base |         removed_packages: list[str] = []  # list of packages which have been removed from the base | ||||||
|         updates = self.load_archives(packages) |         updates = self.load_archives(packages) | ||||||
| @ -202,7 +201,7 @@ class Executor(PackageInfo, Cleaner): | |||||||
|  |  | ||||||
|         result = Result() |         result = Result() | ||||||
|         for local in updates: |         for local in updates: | ||||||
|             with self.in_package_context(local.base, local_versions.get(local.base)): |             with self.in_package_context(local.base, local.version): | ||||||
|                 try: |                 try: | ||||||
|                     packager = self.packager(packagers, local.base) |                     packager = self.packager(packagers, local.base) | ||||||
|  |  | ||||||
|  | |||||||
| @ -144,8 +144,7 @@ class UpdateHandler(PackageInfo, Cleaner): | |||||||
|                         branch="master", |                         branch="master", | ||||||
|                     ) |                     ) | ||||||
|  |  | ||||||
|                     with self.suppress_logging(): |                     Sources.fetch(cache_dir, source) | ||||||
|                         Sources.fetch(cache_dir, source) |  | ||||||
|                     remote = Package.from_build(cache_dir, self.architecture, None) |                     remote = Package.from_build(cache_dir, self.architecture, None) | ||||||
|  |  | ||||||
|                     local = packages.get(remote.base) |                     local = packages.get(remote.base) | ||||||
|  | |||||||
| @ -115,6 +115,14 @@ class Client: | |||||||
|         """ |         """ | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
|  |  | ||||||
|  |     def logs_rotate(self, keep_last_records: int) -> None: | ||||||
|  |         """ | ||||||
|  |         remove older logs from storage | ||||||
|  |  | ||||||
|  |         Args: | ||||||
|  |             keep_last_records(int): number of last records to keep | ||||||
|  |         """ | ||||||
|  |  | ||||||
|     def package_changes_get(self, package_base: str) -> Changes: |     def package_changes_get(self, package_base: str) -> Changes: | ||||||
|         """ |         """ | ||||||
|         get package changes |         get package changes | ||||||
| @ -197,7 +205,8 @@ class Client: | |||||||
|         """ |         """ | ||||||
|         # this method does not raise NotImplementedError because it is actively used as dummy client for http log |         # this method does not raise NotImplementedError because it is actively used as dummy client for http log | ||||||
|  |  | ||||||
|     def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]: |     def package_logs_get(self, package_base: str, limit: int = -1, | ||||||
|  |                          offset: int = 0) -> list[tuple[LogRecordId, float, str]]: | ||||||
|         """ |         """ | ||||||
|         get package logs |         get package logs | ||||||
|  |  | ||||||
| @ -207,7 +216,7 @@ class Client: | |||||||
|             offset(int, optional): records offset (Default value = 0) |             offset(int, optional): records offset (Default value = 0) | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             list[tuple[float, str]]: package logs |             list[tuple[LogRecordId, float, str]]: package logs | ||||||
|  |  | ||||||
|         Raises: |         Raises: | ||||||
|             NotImplementedError: not implemented method |             NotImplementedError: not implemented method | ||||||
|  | |||||||
| @ -75,6 +75,15 @@ class LocalClient(Client): | |||||||
|         """ |         """ | ||||||
|         return self.database.event_get(event, object_id, from_date, to_date, limit, offset, self.repository_id) |         return self.database.event_get(event, object_id, from_date, to_date, limit, offset, self.repository_id) | ||||||
|  |  | ||||||
|  |     def logs_rotate(self, keep_last_records: int) -> None: | ||||||
|  |         """ | ||||||
|  |         remove older logs from storage | ||||||
|  |  | ||||||
|  |         Args: | ||||||
|  |             keep_last_records(int): number of last records to keep | ||||||
|  |         """ | ||||||
|  |         self.database.logs_rotate(keep_last_records, self.repository_id) | ||||||
|  |  | ||||||
|     def package_changes_get(self, package_base: str) -> Changes: |     def package_changes_get(self, package_base: str) -> Changes: | ||||||
|         """ |         """ | ||||||
|         get package changes |         get package changes | ||||||
| @ -145,7 +154,8 @@ class LocalClient(Client): | |||||||
|         """ |         """ | ||||||
|         self.database.logs_insert(log_record_id, created, message, self.repository_id) |         self.database.logs_insert(log_record_id, created, message, self.repository_id) | ||||||
|  |  | ||||||
|     def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]: |     def package_logs_get(self, package_base: str, limit: int = -1, | ||||||
|  |                          offset: int = 0) -> list[tuple[LogRecordId, float, str]]: | ||||||
|         """ |         """ | ||||||
|         get package logs |         get package logs | ||||||
|  |  | ||||||
| @ -155,7 +165,7 @@ class LocalClient(Client): | |||||||
|             offset(int, optional): records offset (Default value = 0) |             offset(int, optional): records offset (Default value = 0) | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             list[tuple[float, str]]: package logs |             list[tuple[LogRecordId, float, str]]: package logs | ||||||
|         """ |         """ | ||||||
|         return self.database.logs_get(package_base, limit, offset, self.repository_id) |         return self.database.logs_get(package_base, limit, offset, self.repository_id) | ||||||
|  |  | ||||||
|  | |||||||
| @ -53,9 +53,6 @@ class Watcher(LazyLogging): | |||||||
|         self._known: dict[str, tuple[Package, BuildStatus]] = {} |         self._known: dict[str, tuple[Package, BuildStatus]] = {} | ||||||
|         self.status = BuildStatus() |         self.status = BuildStatus() | ||||||
|  |  | ||||||
|         # special variables for updating logs |  | ||||||
|         self._last_log_record_id = LogRecordId("", "") |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def packages(self) -> list[tuple[Package, BuildStatus]]: |     def packages(self) -> list[tuple[Package, BuildStatus]]: | ||||||
|         """ |         """ | ||||||
| @ -81,6 +78,8 @@ class Watcher(LazyLogging): | |||||||
|                 for package, status in self.client.package_get(None) |                 for package, status in self.client.package_get(None) | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |     logs_rotate: Callable[[int], None] | ||||||
|  |  | ||||||
|     package_changes_get: Callable[[str], Changes] |     package_changes_get: Callable[[str], Changes] | ||||||
|  |  | ||||||
|     package_changes_update: Callable[[str, Changes], None] |     package_changes_update: Callable[[str, Changes], None] | ||||||
| @ -108,22 +107,9 @@ class Watcher(LazyLogging): | |||||||
|         except KeyError: |         except KeyError: | ||||||
|             raise UnknownPackageError(package_base) from None |             raise UnknownPackageError(package_base) from None | ||||||
|  |  | ||||||
|     def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None: |     package_logs_add: Callable[[LogRecordId, float, str], None] | ||||||
|         """ |  | ||||||
|         make new log record into database |  | ||||||
|  |  | ||||||
|         Args: |     package_logs_get: Callable[[str, int, int], list[tuple[LogRecordId, float, str]]] | ||||||
|             log_record_id(LogRecordId): log record id |  | ||||||
|             created(float): log created timestamp |  | ||||||
|             message(str): log message |  | ||||||
|         """ |  | ||||||
|         if self._last_log_record_id != log_record_id: |  | ||||||
|             # there is new log record, so we remove old ones |  | ||||||
|             self.package_logs_remove(log_record_id.package_base, log_record_id.version) |  | ||||||
|         self._last_log_record_id = log_record_id |  | ||||||
|         self.client.package_logs_add(log_record_id, created, message) |  | ||||||
|  |  | ||||||
|     package_logs_get: Callable[[str, int, int], list[tuple[float, str]]] |  | ||||||
|  |  | ||||||
|     package_logs_remove: Callable[[str, str | None], None] |     package_logs_remove: Callable[[str, str | None], None] | ||||||
|  |  | ||||||
|  | |||||||
| @ -210,6 +210,18 @@ class WebClient(Client, SyncAhrimanClient): | |||||||
|  |  | ||||||
|         return [] |         return [] | ||||||
|  |  | ||||||
|  |     def logs_rotate(self, keep_last_records: int) -> None: | ||||||
|  |         """ | ||||||
|  |         remove older logs from storage | ||||||
|  |  | ||||||
|  |         Args: | ||||||
|  |             keep_last_records(int): number of last records to keep | ||||||
|  |         """ | ||||||
|  |         query = self.repository_id.query() + [("keep_last_records", str(keep_last_records))] | ||||||
|  |  | ||||||
|  |         with contextlib.suppress(Exception): | ||||||
|  |             self.make_request("DELETE", f"{self.address}/api/v1/service/logs", params=query) | ||||||
|  |  | ||||||
|     def package_changes_get(self, package_base: str) -> Changes: |     def package_changes_get(self, package_base: str) -> Changes: | ||||||
|         """ |         """ | ||||||
|         get package changes |         get package changes | ||||||
| @ -306,6 +318,7 @@ class WebClient(Client, SyncAhrimanClient): | |||||||
|         payload = { |         payload = { | ||||||
|             "created": created, |             "created": created, | ||||||
|             "message": message, |             "message": message, | ||||||
|  |             "process_id": log_record_id.process_id, | ||||||
|             "version": log_record_id.version, |             "version": log_record_id.version, | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @ -315,7 +328,8 @@ class WebClient(Client, SyncAhrimanClient): | |||||||
|         self.make_request("POST", self._logs_url(log_record_id.package_base), |         self.make_request("POST", self._logs_url(log_record_id.package_base), | ||||||
|                           params=self.repository_id.query(), json=payload, suppress_errors=True) |                           params=self.repository_id.query(), json=payload, suppress_errors=True) | ||||||
|  |  | ||||||
|     def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]: |     def package_logs_get(self, package_base: str, limit: int = -1, | ||||||
|  |                          offset: int = 0) -> list[tuple[LogRecordId, float, str]]: | ||||||
|         """ |         """ | ||||||
|         get package logs |         get package logs | ||||||
|  |  | ||||||
| @ -325,7 +339,7 @@ class WebClient(Client, SyncAhrimanClient): | |||||||
|             offset(int, optional): records offset (Default value = 0) |             offset(int, optional): records offset (Default value = 0) | ||||||
|  |  | ||||||
|         Returns: |         Returns: | ||||||
|             list[tuple[float, str]]: package logs |             list[tuple[LogRecordId, float, str]]: package logs | ||||||
|         """ |         """ | ||||||
|         query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))] |         query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))] | ||||||
|  |  | ||||||
| @ -333,7 +347,13 @@ class WebClient(Client, SyncAhrimanClient): | |||||||
|             response = self.make_request("GET", self._logs_url(package_base), params=query) |             response = self.make_request("GET", self._logs_url(package_base), params=query) | ||||||
|             response_json = response.json() |             response_json = response.json() | ||||||
|  |  | ||||||
|             return [(record["created"], record["message"]) for record in response_json] |             return [ | ||||||
|  |                 ( | ||||||
|  |                     LogRecordId(package_base, record["version"], record["process_id"]), | ||||||
|  |                     record["created"], | ||||||
|  |                     record["message"] | ||||||
|  |                 ) for record in response_json | ||||||
|  |             ] | ||||||
|  |  | ||||||
|         return [] |         return [] | ||||||
|  |  | ||||||
|  | |||||||
| @ -17,7 +17,9 @@ | |||||||
| # You should have received a copy of the GNU General Public License | # You should have received a copy of the GNU General Public License | ||||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||||
| # | # | ||||||
| from dataclasses import dataclass | import uuid | ||||||
|  |  | ||||||
|  | from dataclasses import dataclass, field | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass(frozen=True) | @dataclass(frozen=True) | ||||||
| @ -28,7 +30,12 @@ class LogRecordId: | |||||||
|     Attributes: |     Attributes: | ||||||
|         package_base(str): package base for which log record belongs |         package_base(str): package base for which log record belongs | ||||||
|         version(str): package version for which log record belongs |         version(str): package version for which log record belongs | ||||||
|  |         process_id(str, optional): unique process identifier | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     package_base: str |     package_base: str | ||||||
|     version: str |     version: str | ||||||
|  |  | ||||||
|  |     # this is not mistake, this value is kind of global identifier, which is generated | ||||||
|  |     # upon the process start | ||||||
|  |     process_id: str = field(default=str(uuid.uuid4())) | ||||||
|  | |||||||
| @ -429,12 +429,11 @@ class Package(LazyLogging): | |||||||
|         task = Task(self, configuration, repository_id.architecture, paths) |         task = Task(self, configuration, repository_id.architecture, paths) | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             with self.suppress_logging(): |             # create fresh chroot environment, fetch sources and - automagically - update PKGBUILD | ||||||
|                 # create fresh chroot environment, fetch sources and - automagically - update PKGBUILD |             task.init(paths.cache_for(self.base), [], None) | ||||||
|                 task.init(paths.cache_for(self.base), [], None) |             pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD") | ||||||
|                 pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD") |  | ||||||
|  |  | ||||||
|                 return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"]) |             return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"]) | ||||||
|         except Exception: |         except Exception: | ||||||
|             self.logger.exception("cannot determine version of VCS package") |             self.logger.exception("cannot determine version of VCS package") | ||||||
|         finally: |         finally: | ||||||
|  | |||||||
| @ -18,7 +18,8 @@ | |||||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||||
| # | # | ||||||
| from aiohttp.web import HTTPException | from aiohttp.web import HTTPException | ||||||
| from typing import Any, Callable | from collections.abc import Callable | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
| from ahriman.models.user_access import UserAccess | from ahriman.models.user_access import UserAccess | ||||||
| from ahriman.web.apispec import Schema, aiohttp_apispec | from ahriman.web.apispec import Schema, aiohttp_apispec | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ from ahriman.web.schemas.info_schema import InfoSchema | |||||||
| from ahriman.web.schemas.internal_status_schema import InternalStatusSchema | from ahriman.web.schemas.internal_status_schema import InternalStatusSchema | ||||||
| from ahriman.web.schemas.log_schema import LogSchema | from ahriman.web.schemas.log_schema import LogSchema | ||||||
| from ahriman.web.schemas.login_schema import LoginSchema | from ahriman.web.schemas.login_schema import LoginSchema | ||||||
|  | from ahriman.web.schemas.logs_rotate_schema import LogsRotateSchema | ||||||
| from ahriman.web.schemas.logs_schema import LogsSchema | from ahriman.web.schemas.logs_schema import LogsSchema | ||||||
| from ahriman.web.schemas.oauth2_schema import OAuth2Schema | from ahriman.web.schemas.oauth2_schema import OAuth2Schema | ||||||
| from ahriman.web.schemas.package_name_schema import PackageNameSchema | from ahriman.web.schemas.package_name_schema import PackageNameSchema | ||||||
| @ -53,5 +54,4 @@ from ahriman.web.schemas.repository_stats_schema import RepositoryStatsSchema | |||||||
| from ahriman.web.schemas.search_schema import SearchSchema | from ahriman.web.schemas.search_schema import SearchSchema | ||||||
| from ahriman.web.schemas.status_schema import StatusSchema | from ahriman.web.schemas.status_schema import StatusSchema | ||||||
| from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema | from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema | ||||||
| from ahriman.web.schemas.versioned_log_schema import VersionedLogSchema |  | ||||||
| from ahriman.web.schemas.worker_schema import WorkerSchema | from ahriman.web.schemas.worker_schema import WorkerSchema | ||||||
|  | |||||||
| @ -17,12 +17,13 @@ | |||||||
| # You should have received a copy of the GNU General Public License | # You should have received a copy of the GNU General Public License | ||||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||||
| # | # | ||||||
|  | from ahriman import __version__ | ||||||
| from ahriman.web.apispec import Schema, fields | from ahriman.web.apispec import Schema, fields | ||||||
|  |  | ||||||
|  |  | ||||||
| class LogSchema(Schema): | class LogSchema(Schema): | ||||||
|     """ |     """ | ||||||
|     request package log schema |     request and response package log schema | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     created = fields.Float(required=True, metadata={ |     created = fields.Float(required=True, metadata={ | ||||||
| @ -32,3 +33,10 @@ class LogSchema(Schema): | |||||||
|     message = fields.String(required=True, metadata={ |     message = fields.String(required=True, metadata={ | ||||||
|         "description": "Log message", |         "description": "Log message", | ||||||
|     }) |     }) | ||||||
|  |     version = fields.String(required=True, metadata={ | ||||||
|  |         "description": "Package version to tag", | ||||||
|  |         "example": __version__, | ||||||
|  |     }) | ||||||
|  |     process_id = fields.String(metadata={ | ||||||
|  |         "description": "Process unique identifier", | ||||||
|  |     }) | ||||||
|  | |||||||
| @ -17,18 +17,14 @@ | |||||||
| # You should have received a copy of the GNU General Public License | # You should have received a copy of the GNU General Public License | ||||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||||
| # | # | ||||||
| from ahriman import __version__ | from ahriman.web.apispec import Schema, fields | ||||||
| from ahriman.web.apispec import fields |  | ||||||
| from ahriman.web.schemas.log_schema import LogSchema |  | ||||||
| from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class VersionedLogSchema(LogSchema, RepositoryIdSchema): | class LogsRotateSchema(Schema): | ||||||
|     """ |     """ | ||||||
|     request package log schema |     request logs rotate schema | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     version = fields.Integer(required=True, metadata={ |     keep_last_records = fields.Integer(metadata={ | ||||||
|         "description": "Package version to tag", |         "description": "Keep the specified amount of records", | ||||||
|         "example": __version__, |  | ||||||
|     }) |     }) | ||||||
| @ -24,8 +24,7 @@ from ahriman.core.utils import pretty_datetime | |||||||
| from ahriman.models.log_record_id import LogRecordId | from ahriman.models.log_record_id import LogRecordId | ||||||
| from ahriman.models.user_access import UserAccess | from ahriman.models.user_access import UserAccess | ||||||
| from ahriman.web.apispec.decorators import apidocs | from ahriman.web.apispec.decorators import apidocs | ||||||
| from ahriman.web.schemas import LogsSchema, PackageNameSchema, PackageVersionSchema, RepositoryIdSchema, \ | from ahriman.web.schemas import LogSchema, LogsSchema, PackageNameSchema, PackageVersionSchema, RepositoryIdSchema | ||||||
|     VersionedLogSchema |  | ||||||
| from ahriman.web.views.base import BaseView | from ahriman.web.views.base import BaseView | ||||||
| from ahriman.web.views.status_view_guard import StatusViewGuard | from ahriman.web.views.status_view_guard import StatusViewGuard | ||||||
|  |  | ||||||
| @ -97,7 +96,7 @@ class LogsView(StatusViewGuard, BaseView): | |||||||
|         response = { |         response = { | ||||||
|             "package_base": package_base, |             "package_base": package_base, | ||||||
|             "status": status.view(), |             "status": status.view(), | ||||||
|             "logs": "\n".join(f"[{pretty_datetime(created)}] {message}" for created, message in logs) |             "logs": "\n".join(f"[{pretty_datetime(created)}] {message}" for _, created, message in logs) | ||||||
|         } |         } | ||||||
|         return json_response(response) |         return json_response(response) | ||||||
|  |  | ||||||
| @ -109,7 +108,7 @@ class LogsView(StatusViewGuard, BaseView): | |||||||
|         error_400_enabled=True, |         error_400_enabled=True, | ||||||
|         error_404_description="Repository is unknown", |         error_404_description="Repository is unknown", | ||||||
|         match_schema=PackageNameSchema, |         match_schema=PackageNameSchema, | ||||||
|         body_schema=VersionedLogSchema, |         body_schema=LogSchema, | ||||||
|     ) |     ) | ||||||
|     async def post(self) -> None: |     async def post(self) -> None: | ||||||
|         """ |         """ | ||||||
| @ -129,6 +128,8 @@ class LogsView(StatusViewGuard, BaseView): | |||||||
|         except Exception as ex: |         except Exception as ex: | ||||||
|             raise HTTPBadRequest(reason=str(ex)) |             raise HTTPBadRequest(reason=str(ex)) | ||||||
|  |  | ||||||
|         self.service().package_logs_add(LogRecordId(package_base, version), created, record) |         # either read from process identifier from payload or assign to the current process identifier | ||||||
|  |         process_id = data.get("process_id", LogRecordId("", "").process_id) | ||||||
|  |         self.service().package_logs_add(LogRecordId(package_base, version, process_id), created, record) | ||||||
|  |  | ||||||
|         raise HTTPNoContent |         raise HTTPNoContent | ||||||
|  | |||||||
							
								
								
									
										63
									
								
								src/ahriman/web/views/v1/service/logs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/ahriman/web/views/v1/service/logs.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | # | ||||||
|  | # Copyright (c) 2021-2025 ahriman team. | ||||||
|  | # | ||||||
|  | # This file is part of ahriman | ||||||
|  | # (see https://github.com/arcan1s/ahriman). | ||||||
|  | # | ||||||
|  | # This program is free software: you can redistribute it and/or modify | ||||||
|  | # it under the terms of the GNU General Public License as published by | ||||||
|  | # the Free Software Foundation, either version 3 of the License, or | ||||||
|  | # (at your option) any later version. | ||||||
|  | # | ||||||
|  | # This program is distributed in the hope that it will be useful, | ||||||
|  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | # GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | # You should have received a copy of the GNU General Public License | ||||||
|  | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | # | ||||||
|  | from aiohttp.web import HTTPBadRequest, HTTPNoContent | ||||||
|  |  | ||||||
|  | from ahriman.models.user_access import UserAccess | ||||||
|  | from ahriman.web.apispec.decorators import apidocs | ||||||
|  | from ahriman.web.schemas import LogsRotateSchema | ||||||
|  | from ahriman.web.views.base import BaseView | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LogsView(BaseView): | ||||||
|  |     """ | ||||||
|  |     logs management web view | ||||||
|  |  | ||||||
|  |     Attributes: | ||||||
|  |         DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     DELETE_PERMISSION = UserAccess.Full | ||||||
|  |     ROUTES = ["/api/v1/service/logs"] | ||||||
|  |  | ||||||
|  |     @apidocs( | ||||||
|  |         tags=["Actions"], | ||||||
|  |         summary="Rotate logs", | ||||||
|  |         description="Remove older logs from system", | ||||||
|  |         permission=DELETE_PERMISSION, | ||||||
|  |         error_400_enabled=True, | ||||||
|  |         error_404_description="Repository is unknown", | ||||||
|  |         query_schema=LogsRotateSchema, | ||||||
|  |     ) | ||||||
|  |     async def delete(self) -> None: | ||||||
|  |         """ | ||||||
|  |         rotate logs from system | ||||||
|  |  | ||||||
|  |         Raises: | ||||||
|  |             HTTPBadRequest: if bad data is supplied | ||||||
|  |             HTTPNoContent: on success response | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             keep_last_records = int(self.request.query.get("keep_last_records", 0)) | ||||||
|  |         except Exception as ex: | ||||||
|  |             raise HTTPBadRequest(reason=str(ex)) | ||||||
|  |  | ||||||
|  |         self.service().logs_rotate(keep_last_records) | ||||||
|  |  | ||||||
|  |         raise HTTPNoContent | ||||||
| @ -67,6 +67,8 @@ class LogsView(StatusViewGuard, BaseView): | |||||||
|             { |             { | ||||||
|                 "created": created, |                 "created": created, | ||||||
|                 "message": message, |                 "message": message, | ||||||
|             } for created, message in logs |                 "version": log_record_id.version, | ||||||
|  |                 "process_id": log_record_id.process_id, | ||||||
|  |             } for log_record_id, created, message in logs | ||||||
|         ] |         ] | ||||||
|         return json_response(response) |         return json_response(response) | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user