Skip to content

Commit 026fc9e

Browse files
committed
python: add flux.eventlog.EventLogFormatter class
Problem: Python utilities do not have access to the eventlog formatter class used by all C utilities for standard eventlog presentation. Add a Python EventLogFormatter class that mimics the C version.
1 parent aa3f5ee commit 026fc9e

File tree

1 file changed

+149
-0
lines changed

1 file changed

+149
-0
lines changed

src/bindings/python/flux/eventlog.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
###############################################################
1010

1111
import json
12+
import sys
13+
from datetime import datetime, timedelta
1214

1315

1416
class EventLogEvent(dict):
@@ -48,3 +50,150 @@ def context_string(self):
4850
return json.dumps(
4951
self.context, ensure_ascii=False, separators=(",", ":"), sort_keys=True
5052
)
53+
54+
55+
class EventLogFormatter:
56+
"""Formatter for eventlog event entries
57+
58+
This class is used by utilities to format eventlog entries in
59+
with optional formatting such as colorization, human-readable and
60+
offset timestamps, etc.
61+
"""
62+
63+
BOLD = "\033[1m"
64+
YELLOW = "\033[33m"
65+
GREEN = "\033[32m"
66+
BLUE = "\033[34m"
67+
MAGENTA = "\033[35m"
68+
GRAY = "\033[37m"
69+
RED = "\033[31m"
70+
71+
eventlog_colors = {
72+
"name": YELLOW,
73+
"time": GREEN,
74+
"timebreak": BOLD + GREEN,
75+
"key": BLUE,
76+
"value": MAGENTA,
77+
"number": GRAY,
78+
"exception": BOLD + RED,
79+
}
80+
81+
def __init__(self, format="text", timestamp_format="raw", color="auto"):
82+
"""Initialize an eventlog formatter
83+
84+
Args:
85+
format (str): The format to use for the eventlog entry. Valid
86+
values include "text" (default), or "json".
87+
timestamp_format (str): The format of the timestamp. Valid values
88+
include "raw" (default), "iso", "offset", "human"
89+
color (str): When to use color. Valid values include "auto" (the
90+
default, "always" or "never".
91+
"""
92+
93+
self.t0 = None
94+
self.last_dt = None
95+
self.last_ts = None
96+
97+
if format not in ("text", "json"):
98+
raise ValueError(f"Invalid entry_fmt: {format}")
99+
self.entry_format = format
100+
101+
if timestamp_format not in ("raw", "human", "reltime", "iso", "offset"):
102+
raise ValueError(f"Invalid timestamp_fmt: {timestamp_format}")
103+
if timestamp_format == "reltime":
104+
timestamp_format = "human"
105+
self.timestamp_format = timestamp_format
106+
107+
if color not in ("always", "never", "auto"):
108+
raise ValueError(f"Invalid color: {color}")
109+
if color == "always":
110+
self.color = True
111+
elif color == "never":
112+
self.color = False
113+
elif color == "auto":
114+
self.color = sys.stdout.isatty()
115+
116+
def _color(self, name):
117+
if self.color:
118+
return self.eventlog_colors[name]
119+
return ""
120+
121+
def _reset(self):
122+
if self.color:
123+
return "\033[0m"
124+
return ""
125+
126+
def _timestamp_human(self, ts):
127+
dt = datetime.fromtimestamp(ts)
128+
129+
if self.last_dt is not None:
130+
delta = dt - self.last_dt
131+
if delta < timedelta(minutes=1):
132+
sec = delta.total_seconds()
133+
return self._color("time") + f"[{sec:>+11.6f}]" + self._reset()
134+
# New minute. Save last timestamp, print dt
135+
self.last_ts = ts
136+
self.last_dt = dt
137+
138+
mday = dt.astimezone().strftime("%b%d %H:%M")
139+
return self._color("timebreak") + f"[{mday}]" + self._reset()
140+
141+
def _timestamp(self, ts):
142+
if self.t0 is None:
143+
self.t0 = ts
144+
145+
if self.timestamp_format == "human":
146+
return self._timestamp_human(ts)
147+
148+
if self.timestamp_format == "raw":
149+
result = f"{ts:11.6f}"
150+
elif self.timestamp_format == "iso":
151+
dt = datetime.fromtimestamp(ts).astimezone()
152+
tz = dt.strftime("%z").replace("+0000", "Z")
153+
us = int((ts - int(ts)) * 1e6)
154+
result = dt.astimezone().strftime("%Y-%m-%dT%T.") + f"{us:06d}" + tz
155+
else: # offset
156+
ts -= self.t0
157+
result = f"{ts:15.6f}"
158+
159+
return self._color("time") + result + self._reset()
160+
161+
def _format_text(self, entry):
162+
ts = self._timestamp(entry.timestamp)
163+
if entry.name == "exception":
164+
name = self._color("exception") + entry.name + self._reset()
165+
else:
166+
name = self._color("name") + entry.name + self._reset()
167+
context = ""
168+
for key, val in entry.context.items():
169+
key = self._color("key") + key + self._reset()
170+
if type(val) in (int, float):
171+
color = self._color("number")
172+
else:
173+
color = self._color("value")
174+
val = color + json.dumps(val, separators=(",", ":")) + self._reset()
175+
context += f" {key}={val}"
176+
177+
return f"{ts} {name}{context}"
178+
179+
def _format_json(self, entry):
180+
# remove context if it is empty
181+
if "context" in entry and not entry["context"]:
182+
entry = {k: v for k, v in entry.items() if k != "context"}
183+
return json.dumps(entry, separators=(",", ":"))
184+
185+
def format(self, entry):
186+
"""Format an eventlog entry
187+
Args:
188+
entry (:obj:`dict` or :obj:`EventLogEntry`): The entry to format.
189+
If a :obj:`dict`, then the entry must conform to RFC 18.
190+
191+
Returns:
192+
str: The formatted eventlog entry as a string.
193+
"""
194+
195+
if not isinstance(entry, EventLogEvent):
196+
entry = EventLogEvent(entry)
197+
if self.entry_format == "json":
198+
return self._format_json(entry)
199+
return self._format_text(entry)

0 commit comments

Comments
 (0)