Extended package status page (#76)

* implement log storage at backend
* handle process id during removal. During one process we can write logs from different packages in different times (e.g. check and update later) and we would like to store all logs belong to the same process
* set package context in main functions
* implement logs support in interface
* filter out logs posting http logs
* add timestamp to log records
* hide getting logs under reporter permission

List of breaking changes:

* `ahriman.core.lazy_logging.LazyLogging` has been renamed to `ahriman.core.log.LazyLogging`
* `ahriman.core.configuration.Configuration.from_path` does not have `quiet` attribute now
* `ahriman.core.configuration.Configuration` class does not have `load_logging` method now
* `ahriman.core.status.client.Client.load` requires `report` argument now
This commit is contained in:
2022-11-22 02:58:22 +03:00
committed by GitHub
parent 8a6854c867
commit 137d62e2f8
90 changed files with 1650 additions and 360 deletions

View File

@ -25,6 +25,7 @@ from ahriman.web.views.service.add import AddView
from ahriman.web.views.service.remove import RemoveView
from ahriman.web.views.service.request import RequestView
from ahriman.web.views.service.search import SearchView
from ahriman.web.views.status.logs import LogsView
from ahriman.web.views.status.package import PackageView
from ahriman.web.views.status.packages import PackagesView
from ahriman.web.views.status.status import StatusView
@ -61,6 +62,10 @@ def setup_routes(application: Application, static_path: Path) -> None:
* ``GET /api/v1/package/:base`` get package base status
* ``POST /api/v1/package/:base`` update package base status
* ``DELETE /api/v1/packages/{package}/logs`` delete package related logs
* ``GET /api/v1/packages/{package}/logs`` create log record for the package
* ``POST /api/v1/packages/{package}/logs`` get last package logs
* ``GET /api/v1/status`` get service status itself
* ``POST /api/v1/status`` update service status itself
@ -94,6 +99,10 @@ def setup_routes(application: Application, static_path: Path) -> None:
application.router.add_get("/api/v1/packages/{package}", PackageView, allow_head=True)
application.router.add_post("/api/v1/packages/{package}", PackageView)
application.router.add_delete("/api/v1/packages/{package}/logs", LogsView)
application.router.add_get("/api/v1/packages/{package}/logs", LogsView, allow_head=True)
application.router.add_post("/api/v1/packages/{package}/logs", LogsView)
application.router.add_get("/api/v1/status", StatusView, allow_head=True)
application.router.add_post("/api/v1/status", StatusView)

View File

@ -0,0 +1,105 @@
#
# Copyright (c) 2021-2022 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, Response, json_response
from aiohttp.web_exceptions import HTTPNotFound
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class LogsView(BaseView):
"""
package logs web view
Attributes:
DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Reporter
async def delete(self) -> None:
"""
delete package logs
Raises:
HTTPNoContent: on success response
"""
package_base = self.request.match_info["package"]
self.service.remove_logs(package_base, None)
raise HTTPNoContent()
async def get(self) -> Response:
"""
get last package logs
Returns:
Response: 200 with package logs on success
"""
package_base = self.request.match_info["package"]
try:
_, status = self.service.get(package_base)
except UnknownPackageError:
raise HTTPNotFound()
logs = self.service.get_logs(package_base)
response = {
"package_base": package_base,
"status": status.view(),
"logs": logs
}
return json_response(response)
async def post(self) -> None:
"""
create new package log record
JSON body must be supplied, the following model is used::
{
"created": 42.001, # log record created timestamp
"message": "log message", # log record
"process_id": 42 # process id from which log record was emitted
}
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
"""
package_base = self.request.match_info["package"]
data = await self.extract_data()
try:
created = data["created"]
record = data["message"]
process_id = data["process_id"]
except Exception as e:
raise HTTPBadRequest(reason=str(e))
self.service.update_logs(LogRecordId(package_base, process_id), created, record)
raise HTTPNoContent()

View File

@ -40,6 +40,18 @@ class PackageView(BaseView):
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
async def delete(self) -> None:
"""
delete package base from status page
Raises:
HTTPNoContent: on success response
"""
package_base = self.request.match_info["package"]
self.service.remove(package_base)
raise HTTPNoContent()
async def get(self) -> Response:
"""
get current package base status
@ -50,10 +62,10 @@ class PackageView(BaseView):
Raises:
HTTPNotFound: if no package was found
"""
base = self.request.match_info["package"]
package_base = self.request.match_info["package"]
try:
package, status = self.service.get(base)
package, status = self.service.get(package_base)
except UnknownPackageError:
raise HTTPNotFound()
@ -65,18 +77,6 @@ class PackageView(BaseView):
]
return json_response(response)
async def delete(self) -> None:
"""
delete package base from status page
Raises:
HTTPNoContent: on success response
"""
base = self.request.match_info["package"]
self.service.remove(base)
raise HTTPNoContent()
async def post(self) -> None:
"""
update package build status
@ -93,7 +93,7 @@ class PackageView(BaseView):
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
"""
base = self.request.match_info["package"]
package_base = self.request.match_info["package"]
data = await self.extract_data()
try:
@ -103,8 +103,8 @@ class PackageView(BaseView):
raise HTTPBadRequest(reason=str(e))
try:
self.service.update(base, status, package)
self.service.update(package_base, status, package)
except UnknownPackageError:
raise HTTPBadRequest(reason=f"Package {base} is unknown, but no package body set")
raise HTTPBadRequest(reason=f"Package {package_base} is unknown, but no package body set")
raise HTTPNoContent()

View File

@ -27,6 +27,7 @@ from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import InitializeError
from ahriman.core.log.filtered_access_logger import FilteredAccessLogger
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
from ahriman.web.middlewares.exception_handler import exception_handler
@ -79,7 +80,7 @@ def run_server(application: web.Application) -> None:
port = configuration.getint("web", "port")
web.run_app(application, host=host, port=port, handle_signals=False,
access_log=logging.getLogger("http"))
access_log=logging.getLogger("http"), access_log_class=FilteredAccessLogger)
def setup_service(architecture: str, configuration: Configuration, spawner: Spawn) -> web.Application: