Compare commits

...

2 Commits

Author SHA1 Message Date
697c585ebd refine package logging 2025-02-14 14:45:45 +02:00
fe1d518171 type: remove unused ignore directive 2025-02-06 12:33:48 +02:00
22 changed files with 259 additions and 83 deletions

View File

@ -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.

View File

@ -57,7 +57,7 @@ class ConfigurationMultiDict(dict[str, Any]):
OptionError: if the key already exists in the dictionary, but not a single value list or a string OptionError: if the key already exists in the dictionary, but not a single value list or a string
""" """
match self.get(key): match self.get(key):
case [current_value] | str(current_value): # type: ignore[misc] case [current_value] | str(current_value):
value = f"{current_value} {value}" value = f"{current_value} {value}"
case None: case None:
pass pass

View File

@ -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",

View 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 ''
""",
]

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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]

View File

@ -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 []

View File

@ -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()))

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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",
})

View File

@ -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__,
}) })

View File

@ -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

View 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

View File

@ -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)