Skip to content

Commit 2c7ca9c

Browse files
author
Paweł Kędzia
committed
Refactor auditor to use pluggable storage backend, add AuditorLogStorageInterface and GPG‑based GPGAuditorLogStorage implementation, and update import path accordingly.
1 parent a110ea6 commit 2c7ca9c

File tree

7 files changed

+243
-66
lines changed

7 files changed

+243
-66
lines changed

llm_router_api/core/auditor.py

Lines changed: 0 additions & 65 deletions
This file was deleted.

llm_router_api/core/auditor/__init__.py

Whitespace-only changes.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
Module providing a simple auditing mechanism for request logs.
3+
4+
The :class:`AnyRequestAuditor` class accepts a standard :class:`logging.Logger`
5+
instance and forwards audit entries to a storage backend defined by
6+
``DEFAULT_AUDITOR_STORAGE_CLASS``. The default implementation stores logs
7+
using GPG encryption via :class:`GPGAuditorLogStorage`.
8+
9+
Typical usage::
10+
11+
logger = logging.getLogger(__name__)
12+
auditor = AnyRequestAuditor(logger)
13+
auditor.add_log({"audit_type": "request", "data": {...}})
14+
15+
"""
16+
17+
import logging
18+
19+
from llm_router_api.core.auditor.log_storage.pgp import GPGAuditorLogStorage
20+
21+
DEFAULT_AUDITOR_STORAGE_CLASS = GPGAuditorLogStorage
22+
23+
24+
class AnyRequestAuditor:
25+
"""Auditor for arbitrary request logs.
26+
27+
Parameters
28+
----------
29+
logger : logging.Logger
30+
Logger used to emit audit notifications. The logger should be
31+
configured by the consuming application.
32+
33+
Attributes
34+
----------
35+
logger : logging.Logger
36+
The logger instance provided at construction.
37+
_auditor_storage : GPGAuditorLogStorage
38+
Instance of the storage backend used to persist audit logs.
39+
"""
40+
41+
def __init__(self, logger: logging.Logger) -> None:
42+
"""
43+
Create a new :class:`AnyRequestAuditor`.
44+
45+
Parameters
46+
----------
47+
logger : logging.Logger
48+
The logger that will receive audit warnings.
49+
"""
50+
self.logger = logger
51+
self._auditor_storage = DEFAULT_AUDITOR_STORAGE_CLASS()
52+
53+
def add_log(self, log):
54+
"""
55+
Record an audit log entry.
56+
57+
The ``log`` argument may be any JSON‑serialisable object – a ``dict``,
58+
a list of ``dict`` objects, or any other structure that can be
59+
serialized by the underlying storage implementation.
60+
61+
The function extracts the ``audit_type`` field from the log, emits a
62+
warning‑level message via the configured logger, and delegates the
63+
actual persistence to ``_auditor_storage.store_log``.
64+
65+
Parameters
66+
----------
67+
log : dict or list
68+
The audit record(s) to store. Must contain an ``"audit_type"``
69+
key when ``log`` is a mapping.
70+
71+
Raises
72+
------
73+
KeyError
74+
If ``log`` is a mapping and does not contain the required
75+
``"audit_type"`` key.
76+
"""
77+
audit_type = log["audit_type"]
78+
self.logger.warning(
79+
f"[AUDIT] ************ Added {audit_type} audit log! ************ "
80+
)
81+
self._auditor_storage.store_log(audit_log=log, audit_type=audit_type)

llm_router_api/core/auditor/log_storage/__init__.py

Whitespace-only changes.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
Abstract definition for audit‑log storage back‑ends.
3+
4+
Any concrete implementation must inherit from
5+
:class:`AuditorLogStorageInterface` and provide a :meth:`store_log`
6+
method that persists the supplied audit record. This contract allows
7+
different storage strategies (e.g., encrypted files, databases,
8+
cloud buckets) to be swapped transparently in the auditing subsystem.
9+
"""
10+
11+
import abc
12+
13+
14+
class AuditorLogStorageInterface(abc.ABC):
15+
"""
16+
Base class for audit‑log storage implementations.
17+
18+
Sub‑classes are required to implement :meth:`store_log`, which
19+
receives a log entry and its associated ``audit_type``. The
20+
interface is deliberately minimal to keep storage back‑ends
21+
lightweight and interchangeable.
22+
"""
23+
24+
@abc.abstractmethod
25+
def store_log(self, audit_log, audit_type: str):
26+
"""
27+
Persist an audit log entry.
28+
29+
Parameters
30+
----------
31+
audit_log : Any
32+
The audit record to store. It can be any JSON‑serialisable
33+
object (e.g., ``dict``, ``list``) depending on the concrete
34+
storage implementation.
35+
36+
audit_type : str
37+
A string categorising the log entry (e.g., ``"request"``,
38+
``"error"``). Implementations may use this value to route
39+
logs to different locations or apply specific handling.
40+
41+
Raises
42+
------
43+
NotImplementedError
44+
If a subclass does not provide an implementation.
45+
"""
46+
raise NotImplementedError
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""
2+
Implementation of an audit‑log storage backend that encrypts logs using GPG.
3+
4+
The :class:`GPGAuditorLogStorage` class conforms to the
5+
:class:`~llm_router_api.core.auditor.log_storage.log_storage_interface.AuditorLogStorageInterface`
6+
protocol. It writes each log entry to a timestamped file inside
7+
``logs/auditor`` and encrypts the JSON payload with a public GPG key
8+
located in ``resources/keys``. This ensures that audit data is stored
9+
at rest in a confidential, tamper‑evident format.
10+
11+
Typical workflow
12+
----------------
13+
1. An instance of :class:`GPGAuditorLogStorage` is created – the public
14+
key is imported automatically.
15+
2. The :meth:`store_log` method is called with a serialisable ``audit_log``
16+
and an ``audit_type`` string.
17+
3. The log is JSON‑encoded, encrypted with the imported key, and written to
18+
``logs/auditor/<audit_type>__<timestamp>.audit``.
19+
"""
20+
21+
import json
22+
import gnupg
23+
24+
from pathlib import Path
25+
from datetime import datetime
26+
27+
from llm_router_api.core.auditor.log_storage.log_storage_interface import (
28+
AuditorLogStorageInterface,
29+
)
30+
31+
32+
class GPGAuditorLogStorage(AuditorLogStorageInterface):
33+
"""
34+
GPG‑backed storage for audit logs.
35+
36+
The storage writes each log entry to a file under ``DEFAULT_AUDITOR_OUT_DIR``
37+
and encrypts the content with the public key found at
38+
``AUDITOR_PUB_KEY_FILE``. The key is imported once during initialisation.
39+
40+
Attributes
41+
----------
42+
_gpg : gnupg.GPG
43+
Configured GPG instance pointing at ``AUDITOR_PUB_KEY_DIR``.
44+
_import_result : gnupg.ImportResult
45+
Result of importing the public key; contains the fingerprint(s) used
46+
for encryption.
47+
"""
48+
49+
# Base output directory for the auditor
50+
DEFAULT_AUDITOR_OUT_DIR = Path("logs/auditor")
51+
DEFAULT_AUDITOR_OUT_DIR.mkdir(parents=True, exist_ok=True)
52+
53+
AUDITOR_PUB_KEY_DIR = Path("resources/keys")
54+
AUDITOR_PUB_KEY_FILE = AUDITOR_PUB_KEY_DIR / Path("llm-router-auditor-pub.asc")
55+
56+
def __init__(self):
57+
"""
58+
Create a new GPG‑based audit log storage instance.
59+
60+
The constructor loads the public key from ``AUDITOR_PUB_KEY_FILE``.
61+
If the key cannot be imported, a :class:`RuntimeError` is raised.
62+
63+
Raises
64+
------
65+
RuntimeError
66+
If the public key file is missing or contains no importable keys.
67+
"""
68+
69+
self._import_result = None
70+
self._gpg = gnupg.GPG(gnupghome=str(self.AUDITOR_PUB_KEY_DIR))
71+
self._gpg.encoding = "utf-8"
72+
73+
with self.AUDITOR_PUB_KEY_FILE.open("r", encoding="utf-8") as f:
74+
self._import_result = self._gpg.import_keys(f.read())
75+
if not self._import_result.count:
76+
raise RuntimeError(
77+
f"Failed to import public key from {self.AUDITOR_PUB_KEY_FILE}"
78+
)
79+
80+
def store_log(self, audit_log, audit_type: str):
81+
"""
82+
Encrypt and persist an audit log entry.
83+
84+
Parameters
85+
----------
86+
audit_log : Any
87+
JSON‑serialisable object representing the audit record.
88+
audit_type : str
89+
Category of the log (e.g., ``"request"``, ``"error"``). The value
90+
is incorporated into the output filename.
91+
92+
The method creates a filename of the form
93+
``<audit_type>__<YYYYMMDD_HHMMSS.microseconds>.audit`` inside
94+
``DEFAULT_AUDITOR_OUT_DIR``. The log is JSON‑encoded with indentation
95+
for readability, encrypted using the previously imported public key,
96+
and written in ASCII‑armored format.
97+
98+
Raises
99+
------
100+
Exception
101+
Propagates any exception raised by the underlying GPG encryption
102+
or file I/O operations.
103+
"""
104+
date_str = datetime.now().strftime("%Y%m%d_%H%M%S.%f")
105+
out_file_name = f"{audit_type}__{date_str}.audit"
106+
out_file_path = self.DEFAULT_AUDITOR_OUT_DIR / out_file_name
107+
with open(out_file_path, "wt") as f:
108+
audit_str = json.dumps(audit_log, indent=2, ensure_ascii=False)
109+
encrypted_data = self._gpg.encrypt(
110+
audit_str,
111+
recipients=self._import_result.fingerprints,
112+
always_trust=True,
113+
armor=True,
114+
)
115+
f.write(str(encrypted_data))

llm_router_api/endpoints/endpoint_i.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
GUARDRAIL_WITH_AUDIT_RESPONSE,
5353
)
5454

55-
from llm_router_api.core.auditor import AnyRequestAuditor
55+
from llm_router_api.core.auditor.auditor import AnyRequestAuditor
5656
from llm_router_api.core.api_types.openai import OPENAI_ACCEPTABLE_PARAMS
5757
from llm_router_api.core.api_types.dispatcher import ApiTypesDispatcher, API_TYPES
5858
from llm_router_api.endpoints.httprequest import HttpRequestExecutor

0 commit comments

Comments
 (0)