Skip to content

Commit 12710f1

Browse files
authored
Audit log formatter (distributed-system-analysis#3611)
* Audit log formatter Another late night ops side project: this queries the `Audit` log with filters and displays the results either in detail or summary, optionally with pagination. ``` /opt/pbench-server/bin/pbench-audit --since 2024-02-29 --until 2024-03-01 --name upload --summary [...] [2024-02-29 19:35:07] upload uperf_HyperV_RHEL-9.4.0-20240225.8_x86_64_gen2_hv_netvsc_quick_D240227T172431_2024.02.27T09.24.31 FAILURE (legacy) [2.584 seconds] [2024-02-29 19:36:09] upload uperf_HyperV_RHEL-9.4.0-20240225.8_x86_64_gen2_hv_netvsc_quick_D240227T172431_2024.02.27T09.24.31 SUCCESS (legacy) [4.093 seconds] Reported 428 audit events: BEGIN 214 SUCCESS 24 FAILURE 190 ``` Or (full form): ``` /opt/pbench-server/bin/pbench-audit -C /opt/pbench-server/lib/config/pbench-server.cfg --since 2024-02-29T12:00:00 --until '2024-02-29 19:00:00' --name upload [...] [2024-02-29 18:58:09.608043+00:00] : upload BEGIN DATASET uperf_HyperV_RHEL-9.4.0-20240225.8_x86_64_gen2_hv_netvsc_quick_D240227T172431_2024.02.27T09.24.31 by user legacy {'access': 'public', 'metadata': {'global.server.legacy.version': '0.69.11', 'global.server.legacy.sha1': '29bf18093', 'global.server.legacy.hostname': 'n010.intlab.redhat.com'}} [2024-02-29 18:58:12.279230+00:00] : upload FAILURE [2.671 seconds] DATASET uperf_HyperV_RHEL-9.4.0-20240225.8_x86_64_gen2_hv_netvsc_quick_D240227T172431_2024.02.27T09.24.31 by user legacy {'message': "A tarball with the name 'uperf_HyperV_RHEL-9.4.0-20240225.8_x86_64_gen2_hv_netvsc_quick_D240227T172431_2024.02.27T09.24.31' already exists"} [2024-02-29 18:59:05.843728+00:00] : upload BEGIN DATASET uperf_HyperV_RHEL-9.4.0-20240225.8_x86_64_gen2_hv_netvsc_quick_D240227T172431_2024.02.27T09.24.31 by user legacy {'access': 'public', 'metadata': {'global.server.legacy.version': '0.69.11', 'global.server.legacy.sha1': '29bf18093', 'global.server.legacy.hostname': 'n010.intlab.redhat.com'}} [2024-02-29 18:59:08.592574+00:00] : upload FAILURE [2.749 seconds] DATASET uperf_HyperV_RHEL-9.4.0-20240225.8_x86_64_gen2_hv_netvsc_quick_D240227T172431_2024.02.27T09.24.31 by user legacy {'message': "A tarball with the name 'uperf_HyperV_RHEL-9.4.0-20240225.8_x86_64_gen2_hv_netvsc_quick_D240227T172431_2024.02.27T09.24.31' already exists"} Reported 354 audit events: BEGIN 177 SUCCESS 23 FAILURE 154 ```
1 parent 906a06a commit 12710f1

File tree

3 files changed

+189
-0
lines changed

3 files changed

+189
-0
lines changed

lib/pbench/cli/server/audit.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
from collections import defaultdict
2+
import datetime
3+
from typing import Iterator, Optional
4+
5+
import click
6+
7+
from pbench.cli import pass_cli_context
8+
from pbench.cli.server import config_setup, Verify
9+
from pbench.cli.server.options import common_options
10+
from pbench.server import BadConfig, OperationCode
11+
from pbench.server.database.database import Database
12+
from pbench.server.database.models.audit import Audit, AuditStatus, AuditType
13+
14+
COLUMNS = (
15+
"id",
16+
"root_id",
17+
"name",
18+
"operation",
19+
"object_type",
20+
"object_id",
21+
"object_name",
22+
"user_id",
23+
"user_name",
24+
"status",
25+
"reason",
26+
)
27+
28+
verifier: Optional[Verify] = None
29+
30+
31+
def auditor(kwargs) -> Iterator[str]:
32+
"""Report audit records matching patterns.
33+
34+
Args:
35+
kwargs: the command options.
36+
37+
Returns:
38+
A sequence of lines to be written
39+
"""
40+
41+
summary = kwargs.get("summary")
42+
open: dict[int, Audit] = {}
43+
query = (
44+
Database.db_session.query(Audit)
45+
.order_by(Audit.timestamp)
46+
.execution_options(stream_results=True)
47+
)
48+
filters = []
49+
for k in COLUMNS:
50+
if kwargs.get(k):
51+
filters.append(getattr(Audit, k) == kwargs.get(k))
52+
since = kwargs.get("since")
53+
until = kwargs.get("until")
54+
if since:
55+
filters.append(Audit.timestamp >= since)
56+
if until:
57+
filters.append(Audit.timestamp <= until)
58+
query = query.filter(*filters)
59+
if not summary:
60+
yield "Audit records:\n"
61+
rows = query.yield_per(2000)
62+
count = 0
63+
status = defaultdict(int)
64+
for audit in rows:
65+
duration = ""
66+
count += 1
67+
status[audit.status] += 1
68+
69+
# If we're showing both start and termination events, we can compute
70+
# the duration of an operation; so save "open" BEGIN event timestamps
71+
# until we reach the matching termination (SUCCESS. FAILURE, WARNING).
72+
if audit.status is AuditStatus.BEGIN:
73+
open[audit.id] = audit
74+
else:
75+
if audit.root_id in open:
76+
delta: datetime.timedelta = (
77+
audit.timestamp - open[audit.root_id].timestamp
78+
)
79+
d = float(delta.seconds) + (delta.microseconds / 1000000.0)
80+
duration = f" [{d:.3f} seconds]"
81+
del open[audit.root_id]
82+
if summary:
83+
if duration:
84+
yield (
85+
f"[{audit.timestamp:%Y-%m-%d %H:%M:%S}] {audit.name} "
86+
f"{audit.object_name} {audit.status.name} ({audit.user_name}){duration}\n"
87+
)
88+
continue
89+
yield f" [{audit.timestamp}] : {audit.name} {audit.status.name}{duration}\n"
90+
yield f" {audit.object_type.name} {audit.object_name} by user {audit.user_name}\n"
91+
if kwargs.get("ids"):
92+
yield (
93+
f" ID {audit.id}, ROOT {audit.root_id}: OBJ "
94+
f"{audit.object_id}, UID {audit.user_id}\n"
95+
)
96+
if audit.attributes:
97+
yield f" {audit.attributes}\n"
98+
yield ""
99+
yield f"Reported {count:,d} audit events:\n"
100+
for s, c in status.items():
101+
yield f" {s.name:>10s} {c:>10,d}\n"
102+
if open:
103+
yield f"{len(open):,d} unterminated events:\n"
104+
for audit in open.values():
105+
yield (
106+
f" [{audit.timestamp:%Y-%m-%d %H:%M:%S}] {audit.id:10d} "
107+
f"{audit.name} {audit.object_name} ({audit.user_name}) {audit.attributes}\n"
108+
)
109+
110+
111+
@click.command(name="pbench-audit")
112+
@pass_cli_context
113+
@click.option(
114+
"--ids",
115+
default=False,
116+
is_flag=True,
117+
help="Show user and object IDs as well as names",
118+
)
119+
@click.option("--name", type=str, help="Select by audit event name")
120+
@click.option(
121+
"--operation",
122+
type=click.Choice([o.name for o in OperationCode], case_sensitive=False),
123+
help="Select by audit operation name",
124+
)
125+
@click.option(
126+
"--object-type",
127+
type=click.Choice([t.name for t in AuditType], case_sensitive=False),
128+
help="Select by object type",
129+
)
130+
@click.option("--object-id", type=str, help="Select by object ID")
131+
@click.option("--object-name", type=str, help="Select by object name")
132+
@click.option("--page", default=False, is_flag=True, help="Paginate the output")
133+
@click.option("--user-id", type=str, help="Select by user ID")
134+
@click.option("--user-name", type=str, help="Select by username")
135+
@click.option(
136+
"--status",
137+
type=click.Choice([s.name for s in AuditStatus], case_sensitive=False),
138+
help="Select by operation status",
139+
)
140+
@click.option(
141+
"--summary", default=False, is_flag=True, help="Show one-line summary of operations"
142+
)
143+
@click.option(
144+
"--since",
145+
type=click.DateTime(),
146+
help="Select entries on or after specified date/time",
147+
)
148+
@click.option(
149+
"--until",
150+
type=click.DateTime(),
151+
help="Select entries on or before specified date/time",
152+
)
153+
@click.option(
154+
"--verify", "-v", default=False, is_flag=True, help="Display intermediate messages"
155+
)
156+
@common_options
157+
def audit(context: object, **kwargs):
158+
"""Query and format the audit DB table
159+
160+
The Audit table records a sequence of events representing all changes made
161+
to the data controlled by the Pbench Server. This tool supports queries to
162+
display audit events based on various search criteria including timestamp,
163+
user, object identification, and others.
164+
\f
165+
166+
Args:
167+
context: click context
168+
kwargs: click options
169+
"""
170+
global verifier
171+
verifier = Verify(kwargs.get("verify"))
172+
173+
try:
174+
config_setup(context)
175+
if kwargs.get("page"):
176+
click.echo_via_pager(auditor(kwargs))
177+
else:
178+
for line in auditor(kwargs):
179+
click.echo(line, nl=False)
180+
rv = 0
181+
except Exception as exc:
182+
if verifier.verify:
183+
raise
184+
click.secho(exc, err=True, bg="red")
185+
rv = 2 if isinstance(exc, BadConfig) else 1
186+
187+
click.get_current_context().exit(rv)

server/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ INSTALL = install
2020
INSTALLOPTS = --directory
2121

2222
click-scripts = \
23+
pbench-audit \
2324
pbench-report-generator \
2425
pbench-tree-manage \
2526
pbench-user-create \

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ where = lib
2424

2525
[entry_points]
2626
console_scripts =
27+
pbench-audit = pbench.cli.server.audit:audit
2728
pbench-cleanup = pbench.cli.agent.commands.cleanup:main
2829
pbench-clear-results = pbench.cli.agent.commands.results.clear:main
2930
pbench-clear-tools = pbench.cli.agent.commands.tools.clear:main

0 commit comments

Comments
 (0)