Skip to content
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

datastream: add basic OS logging #82

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions invenio_logging/datastream.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

from importlib_metadata import entry_points

from . import config
from .datastreams.managers import LogManager
from .ext import InvenioLoggingBase
from .resources import LogsResource, LogsResourceConfig


class InvenioLoggingDatastreams(InvenioLoggingBase):
Expand All @@ -25,11 +25,12 @@ def init_app(self, app):

:param app: An instance of :class:`~flask.Flask`.
"""
self.init_manager(app)
self.init_manager()
self.init_resources(app)
self.load_builders()
app.extensions["invenio-logging-datastreams"] = self

def init_manager(self, app):
def init_manager(self):
"""Initialize the logging manager."""
manager = LogManager()
self.manager = manager
Expand All @@ -39,3 +40,10 @@ def load_builders(self):
for ep in entry_points(group="invenio_logging.datastreams.builders"):
builder_class = ep.load()
self.manager.register_builder(ep.name, builder_class)

def init_resources(self, app):
"""Init resources."""
self.resource = LogsResource(
config=LogsResourceConfig.build(app),
manager=self.manager,
)
6 changes: 6 additions & 0 deletions invenio_logging/datastreams/audit_logs/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ def search(cls, query):
"""Search logs."""
results = cls.backend_cls().search(query)
return cls.schema.dump(results, many=True)

@classmethod
def list(cls):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is list different from search?

"""List audit logs."""
results = cls.backend_cls().list()
return cls.schema.dump(results, many=True)
23 changes: 22 additions & 1 deletion invenio_logging/datastreams/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def search(self, query=None, size=10):
"operator": "and",
}
},
"sort": [{"timestamp": {"order": "desc"}}],
"sort": [{"@timestamp": {"order": "desc"}}],
Copy link
Contributor

@kpsherva kpsherva Mar 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this whole method should be in a log dedicated service class
this should probably not be in this module.
could work as a prototype now, but it should be moved in the next iteration to a service - also because we are missing the permissions completely here.
Probably even before finalising the frontend, otherwise you will end up patching a lot of things.

}
# TODO: add pagination?
response = self.client.search(index=full_index_name, body=search_query)
Expand All @@ -98,3 +98,24 @@ def search(self, query=None, size=10):
except Exception as e:
current_app.logger.error(f"Failed to search logs: {e}")
raise e

def list(self, size=10):
"""
List log events.

:param size: Number of results to return.
:return: List of log events.
"""
try:
index_prefix = current_app.config.get("SEARCH_INDEX_PREFIX", "")
full_index_name = f"{index_prefix}{self.index_name}"

response = self.client.search(
index=full_index_name,
body={"size": size, "sort": [{"@timestamp": {"order": "desc"}}]},
)
return [hit["_source"] for hit in response.get("hits", {}).get("hits", [])]

except Exception as e:
current_app.logger.error(f"Failed to search logs: {e}")
raise e
16 changes: 15 additions & 1 deletion invenio_logging/datastreams/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ def log(self, log_event, async_mode=True):
log_builder.send(log)

def search(self, log_type, query):
"""Search logs."""
"""
Search for logs using the correct builder.
"""
if log_type not in self.builders:
raise ValueError(
f"No log builder found for type '{log_type}'. Available types: {self.builders.keys()}"
Expand All @@ -52,6 +54,18 @@ def search(self, log_type, query):
log_builder = self.builders[log_type]
return log_builder.search(query)

def list(self, log_type):
"""
List logs using the correct builder.
"""
if log_type not in self.builders:
raise ValueError(
f"No log builder found for type '{log_type}'. Available types: {self.builders.keys()}"
)

log_builder = self.builders[log_type]
return log_builder.list()

def register_builder(self, log_type, builder_class):
"""Register a log builder."""
self.builders[log_type] = builder_class
17 changes: 17 additions & 0 deletions invenio_logging/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 CERN.
#
# Invenio-Requests is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Custom exceptions used in the Invenio-Logging module."""


class InvalidLogQueryError(Exception):
"""Error raised when an invalid query is made on logging resources."""

def __init__(self, message="Invalid log query parameters provided."):
"""Initialize error with a default message."""
self.message = message
super().__init__(self.message)
17 changes: 17 additions & 0 deletions invenio_logging/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 CERN.
#
# Invenio-Requests is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.

"""Resources module."""

from .resource import LogsResource
from .config import LogsResourceConfig

__all__ = (
"LogsResource",
"LogsResourceConfig",
)
56 changes: 56 additions & 0 deletions invenio_logging/resources/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from marshmallow import fields

from invenio_records_resources.resources import SearchRequestArgsSchema, RecordResourceConfig
from invenio_records_resources.services.base.config import ConfiguratorMixin

from flask_resources import HTTPJSONException, create_error_handler
from ..errors import InvalidLogQueryError

#
# Request args
#
class LogSearchRequestArgsSchema(SearchRequestArgsSchema):
"""Search parameters for logs."""

resource_id = fields.String()
resource_type = fields.String()
user_id = fields.String()
action = fields.String()

error_handlers = {
InvalidLogQueryError: create_error_handler(
lambda e: HTTPJSONException(code=400, description=str(e))
),
}

#
# Resource config
#
class LogsResourceConfig(RecordResourceConfig, ConfiguratorMixin):
"""Logs resource configuration."""

blueprint_name = "logs"
url_prefix = "/logs"

routes = {
"list": "/",
"item": "/<id>",
}

request_view_args = {
"resource_id": fields.String(),
# "resource_type": fields.String(), # TODO: Add direct querying via other search parameters?
# "user_id": fields.String(),
# "action": fields.String(),
}

request_search_args = LogSearchRequestArgsSchema

error_handlers = error_handlers

response_handlers = {
"application/vnd.inveniordm.v1+json": RecordResourceConfig.response_handlers[
"application/json"
],
**RecordResourceConfig.response_handlers,
}
77 changes: 77 additions & 0 deletions invenio_logging/resources/resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2025 CERN.
#
# Invenio-Logging is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.

"""Logs resource."""

from flask import g
from flask_resources import resource_requestctx, response_handler, route
from flask_resources import Resource
from invenio_records_resources.resources.records.resource import (
# request_data,
request_extra_args,
# request_headers,
request_search_args,
request_view_args,
)
from invenio_records_resources.resources.records.utils import search_preference


#
# Resource
#
class LogsResource(Resource):
"""Resource for logs (audit and job logs)."""

def __init__(self, config, manager):
"""Constructor."""
super(LogsResource, self).__init__(config)
self.manager = manager

def create_blueprint(self, **options):
"""Create the blueprint."""
options["url_prefix"] = ""
return super().create_blueprint(**options)

def create_url_rules(self):
"""Create the URL rules for the log resource."""
routes = self.config.routes

def p(route):
"""Prefix a route with the URL prefix."""
return f"{self.config.url_prefix}{route}"

return [
route("GET", p(routes["list"]), self.search),
route("GET", p(routes["item"]), self.read),
]

@request_extra_args
@request_search_args
@request_view_args
@response_handler(many=True)
def search(self):
"""Perform a search over the logs."""
hits = self.manager.search(
identity=g.identity,
params=resource_requestctx.args,
search_preference=search_preference(),
expand=resource_requestctx.args.get("expand", False),
)
return hits.to_dict(), 200

@request_extra_args
@request_view_args
@response_handler()
def read(self):
"""Read a specific log entry."""
item = self.manager.read_all(
id_=resource_requestctx.view_args["id"],
identity=g.identity,
expand=resource_requestctx.args.get("expand", False),
)
return item.to_dict(), 200
Loading