Skip to content

Commit a48c3a8

Browse files
committed
Made changes to the python scripts for consistency and accuracty:
- Changed user, project and resource variable consisent with loki/prometheus - Make python script params more consistent - Fixed python files to handle mutate: NUMBOOL correctly when calculating rating - Add scenario name to the db push so that will be target db data to pull Authored-by @ayefimov-1 Assisted-by: Claude
1 parent 039583b commit a48c3a8

6 files changed

Lines changed: 176 additions & 66 deletions

File tree

roles/telemetry_chargeback/files/gen_db_summary.py

Lines changed: 90 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def _apply_mutate(qty: float, mutate: str) -> float:
119119
return math.floor(qty)
120120
elif mutate_upper == "NUMBOOL":
121121
# If qty equals 0, leave it at 0. Else, set it to 1.
122-
return 0.0 if qty == 0 else 1.0
122+
return 0.0 if abs(qty) < 1e-9 else 1.0
123123
elif mutate_upper == "NOTNUMBOOL":
124124
# If qty equals 0, set it to 1. Else, set it to 0.
125125
return 1.0 if qty == 0 else 0.0
@@ -175,8 +175,9 @@ def _parse_numeric(value: Any, default: float = 0) -> float:
175175

176176
def aggregate_rates_by_type(
177177
pairs: list[tuple[str, str]],
178-
) -> tuple[dict, float]:
179-
sums: defaultdict[str, float] = defaultdict(float)
178+
) -> tuple[dict, float, dict]:
179+
rate_sums: defaultdict[str, float] = defaultdict(float)
180+
qty_sums: defaultdict[str, float] = defaultdict(float)
180181
for _, log_str in pairs:
181182
try:
182183
entry = json.loads(log_str)
@@ -196,17 +197,26 @@ def aggregate_rates_by_type(
196197
except (TypeError, ValueError):
197198
continue
198199

199-
# Apply mutate transformation
200+
# Track raw qty sum (before any transformation)
201+
qty_sums[mtype] += qty
202+
203+
# Apply mutate transformation for rating calculation
200204
qty_mutated = _apply_mutate(qty, mutate)
201205

202206
# Apply factor and offset
203207
qty_rate = qty_mutated * factor + offset
204208

205209
# Calculate rate
206-
sums[mtype] += qty_rate * price
207-
by_types = {k: {"Rate": round(v, 4)} for k, v in sorted(sums.items())}
208-
total = sum(sums.values())
209-
return by_types, total
210+
rate_sums[mtype] += qty_rate * price
211+
212+
by_types = {
213+
k: {"Rate": round(v, 4)} for k, v in sorted(rate_sums.items())
214+
}
215+
qty_by_types = {
216+
k: {"qty_sum": round(v, 4)} for k, v in sorted(qty_sums.items())
217+
}
218+
total = sum(rate_sums.values())
219+
return by_types, total, qty_by_types
210220

211221

212222
def build_summary(pairs: list[tuple[str, str]]) -> dict[str, Any]:
@@ -237,17 +247,35 @@ def build_summary(pairs: list[tuple[str, str]]) -> dict[str, Any]:
237247
empty = {"nanosec": None, "begin": None, "end": None}
238248
time_block = {"begin_step": empty.copy(), "end_step": empty.copy()}
239249

240-
by_types, total_r = aggregate_rates_by_type(pairs)
250+
# Get aggregated data by type
251+
by_types, total_r, qty_by_types = aggregate_rates_by_type(pairs)
252+
253+
# Get overall time range for by_type entries
254+
begin_time = first.get("start") if pairs else None
255+
end_time = last.get("end") if pairs else None
256+
257+
# Build flat list of entries
258+
rate_list = []
259+
for type_name in sorted(by_types.keys()):
260+
entry = {
261+
"Begin": begin_time,
262+
"End": end_time,
263+
"Qty": qty_by_types.get(type_name, {}).get("qty_sum", 0.0),
264+
"Rate": by_types[type_name]["Rate"],
265+
"Type": type_name,
266+
}
267+
rate_list.append(entry)
268+
241269
return {
242270
"time": time_block,
243-
"data_log": {
271+
"data_summary": {
244272
"total_timesteps": n_ts,
245273
"metrics_per_step": mps,
246274
"log_count": log_count,
275+
"total_rating": round(total_r, 4),
247276
},
248-
"rate": {
249-
"by_types": by_types,
250-
"total": {"Rating": round(total_r, 4)},
277+
"by_type": {
278+
"rate": rate_list,
251279
},
252280
}
253281

@@ -264,10 +292,36 @@ def write_yaml(path: Path, doc: dict[str, Any]) -> None:
264292
)
265293

266294

295+
def _str_to_bool(value: str) -> bool:
296+
"""
297+
Convert string to boolean.
298+
299+
Args:
300+
value: String representation of boolean.
301+
302+
Returns:
303+
Boolean value.
304+
305+
Raises:
306+
argparse.ArgumentTypeError: If value cannot be converted.
307+
"""
308+
if isinstance(value, bool):
309+
return value
310+
if value.lower() in ('yes', 'true', 't', 'y', '1'):
311+
return True
312+
elif value.lower() in ('no', 'false', 'f', 'n', '0'):
313+
return False
314+
else:
315+
raise argparse.ArgumentTypeError(
316+
f"Boolean value expected. Got: {value}"
317+
)
318+
319+
267320
def main() -> None:
268321
parser = argparse.ArgumentParser(
269322
description=(
270-
"Summarize Loki JSON log entries to YAML (time, data_log, rate)."
323+
"Summarize Loki JSON log entries to YAML "
324+
"(time, data_summary, by_type)."
271325
),
272326
)
273327
parser.add_argument(
@@ -282,11 +336,21 @@ def main() -> None:
282336
)
283337
parser.add_argument(
284338
"--debug",
339+
type=_str_to_bool,
340+
default=False,
341+
metavar="BOOL",
342+
help=(
343+
"Enable debug mode: write <stem>_diff.txt with one "
344+
"[ts,log] JSON per line (true/false)."
345+
),
346+
)
347+
parser.add_argument(
348+
"--debug_dir",
285349
type=Path,
286350
default=None,
287351
metavar="DIR",
288352
help=(
289-
"If set, write <stem>_diff.txt with one [ts,log] JSON per line."
353+
"Directory for debug output. Required when --debug is enabled."
290354
),
291355
)
292356
args = parser.parse_args()
@@ -299,24 +363,24 @@ def main() -> None:
299363
out_path = args.output or (args.json.parent / f"{stem}_total.yml")
300364
pairs = extract_and_sort(args.json)
301365

302-
dbg = str(args.debug).strip() if args.debug is not None else ""
303-
if dbg and dbg != ".":
304-
args.debug.mkdir(parents=True, exist_ok=True)
305-
dbg_file = args.debug / f"{args.json.stem}_diff.txt"
366+
if args.debug:
367+
# Require debug directory when debug mode is enabled
368+
if not args.debug_dir:
369+
print(
370+
"Error: --debug_dir is required when --debug is enabled",
371+
file=sys.stderr
372+
)
373+
sys.exit(1)
374+
debug_dir = args.debug_dir
375+
debug_dir.mkdir(parents=True, exist_ok=True)
376+
dbg_file = debug_dir / f"{args.json.stem}_diff.txt"
306377
with dbg_file.open("w", encoding="utf-8") as f:
307378
for ts, log_str in pairs:
308379
print(json.dumps([ts, log_str], ensure_ascii=False), file=f)
309380

310381
doc = build_summary(pairs)
311382
write_yaml(out_path, doc)
312383

313-
if doc["data_log"]["metrics_per_step"] == "ERROR":
314-
per_ts = Counter(ts for ts, _ in pairs)
315-
exp = next(iter(per_ts.values()), 0)
316-
for ts in sorted(per_ts, key=int):
317-
if per_ts[ts] != exp:
318-
print(ts, per_ts[ts], file=sys.stdout)
319-
320384

321385
if __name__ == "__main__":
322386
main()

roles/telemetry_chargeback/files/gen_synth_loki_data.py

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def generate_loki_data(
109109
end_time: datetime,
110110
time_step_seconds: int,
111111
config: Dict[str, Any],
112+
scenario_name: str,
112113
reverse_timestamps: bool = True,
113114
):
114115
"""
@@ -121,6 +122,7 @@ def generate_loki_data(
121122
end_time (datetime): The end time for data generation.
122123
time_step_seconds (int): The duration of each log entry in seconds.
123124
config (Dict[str, Any]): Configuration dictionary loaded from file.
125+
scenario_name (str): Name of the test scenario for Loki labeling.
124126
reverse_timestamps (bool): If True, sort timestamps in descending order
125127
(newest first, oldest last). If False, sort in ascending order
126128
(oldest first, newest last). Default is True (descending).
@@ -138,6 +140,7 @@ def generate_loki_data(
138140
logger.debug(f"Time range in epoch seconds: {start_epoch} to {end_epoch}")
139141

140142
log_data_list = [] # This list will hold all our data points
143+
last_end_of_step_epoch = None # Track last entry's end epoch
141144

142145
# Loop through the time range and generate data points
143146
for current_epoch in range(
@@ -164,13 +167,13 @@ def generate_loki_data(
164167
"end_time": end_str
165168
})
166169

170+
# Track the last end epoch
171+
last_end_of_step_epoch = end_of_step_epoch
172+
167173
# Add final entry that ends at end_epoch (current time)
168-
if log_data_list and end_epoch > start_epoch:
174+
if log_data_list and end_epoch > start_epoch and last_end_of_step_epoch:
169175
# Calculate start of final entry based on end of last generated entry
170-
last_entry_end = log_data_list[-1]["end_time"]
171-
# Parse the last entry's end time to get the epoch
172-
last_end_dt = datetime.fromisoformat(last_entry_end)
173-
final_start_epoch = int(last_end_dt.timestamp()) + 1
176+
final_start_epoch = last_end_of_step_epoch + 1
174177
final_nanoseconds = int(final_start_epoch * 1_000_000_000)
175178

176179
# Only add if the final entry would have a valid duration
@@ -222,8 +225,11 @@ def generate_loki_data(
222225

223226
# Validate required fields
224227
# metadata is optional for generation; name is not a log-type field
225-
required_for_item = set(required_fields) - {"name", "metadata"}
226-
missing = required_for_item - set(log_type_config)
228+
required_for_item = [
229+
f for f in required_fields
230+
if f not in ("name", "metadata")
231+
]
232+
missing = [f for f in required_for_item if f not in log_type_config]
227233
if missing:
228234
logger.error(
229235
f"Missing required fields in {type_key!r} config: {missing}"
@@ -255,7 +261,10 @@ def generate_loki_data(
255261
"qty": qty_list,
256262
"price": price_list,
257263
"groupby": groupby.copy(),
258-
"metadata": log_type_config.get("metadata", {})
264+
"metadata": log_type_config.get("metadata", {}),
265+
"mutate": log_type_config.get("mutate"),
266+
"factor": log_type_config.get("factor"),
267+
"offset": log_type_config.get("offset")
259268
}
260269

261270
# --- Step 3: Load template and render ---
@@ -333,12 +342,16 @@ def tojson_preserve_order(obj):
333342

334343
log_types_list.append(log_types_with_dates)
335344

336-
# Get loki_stream configuration
345+
# Get loki_stream configuration and add scenario
337346
loki_stream = config.get("loki_stream", {})
338347
if not loki_stream:
339348
logger.warning("No loki_stream configuration found, using defaults")
340349
loki_stream = {"service": "cloudkitty"}
341350

351+
# Add scenario name to loki_stream labels
352+
loki_stream["scenario"] = scenario_name
353+
logger.info(f"Adding scenario label: {scenario_name}")
354+
342355
# Build template context with generic log type information
343356
template_context = {
344357
"log_data": log_data_list,
@@ -373,6 +386,31 @@ def tojson_preserve_order(obj):
373386
sys.exit(1)
374387

375388

389+
def _str_to_bool(value: str) -> bool:
390+
"""
391+
Convert string to boolean.
392+
393+
Args:
394+
value: String representation of boolean.
395+
396+
Returns:
397+
Boolean value.
398+
399+
Raises:
400+
argparse.ArgumentTypeError: If value cannot be converted.
401+
"""
402+
if isinstance(value, bool):
403+
return value
404+
if value.lower() in ('yes', 'true', 't', 'y', '1'):
405+
return True
406+
elif value.lower() in ('no', 'false', 'f', 'n', '0'):
407+
return False
408+
else:
409+
raise argparse.ArgumentTypeError(
410+
f"Boolean value expected. Got: {value}"
411+
)
412+
413+
376414
def main():
377415
"""Main entry point for the script."""
378416
parser = argparse.ArgumentParser(
@@ -415,8 +453,10 @@ def main():
415453
)
416454
parser.add_argument(
417455
"--debug",
418-
action="store_true",
419-
help="Enable debug level logging for verbose output."
456+
type=_str_to_bool,
457+
default=False,
458+
metavar="BOOL",
459+
help="Enable debug level logging for verbose output (true/false)."
420460
)
421461
args = parser.parse_args()
422462

@@ -429,7 +469,11 @@ def main():
429469
config = load_config(args.test)
430470
except (FileNotFoundError, ValueError) as e:
431471
logger.critical(f"Failed to load config: {e}")
432-
return
472+
sys.exit(1)
473+
474+
# Derive scenario name from test file path
475+
scenario_name = args.test.stem
476+
logger.info(f"Derived scenario name from test file: {scenario_name}")
433477

434478
# Get generation parameters from config
435479
generation_config = config.get("generation", {})
@@ -450,14 +494,17 @@ def main():
450494
end_time=end_time_utc,
451495
time_step_seconds=step_seconds,
452496
config=config,
497+
scenario_name=scenario_name,
453498
reverse_timestamps=args.reverse,
454499
)
455500
except FileNotFoundError:
456501
logger.error(
457502
"Process aborted because the template file was not found."
458503
)
504+
sys.exit(1)
459505
except Exception as e:
460506
logger.critical(f"A critical, unhandled error stopped the script: {e}")
507+
sys.exit(1)
461508

462509

463510
if __name__ == "__main__":

0 commit comments

Comments
 (0)