Skip to content

Commit 6bd0bf0

Browse files
myadlaayefimov-1
authored andcommitted
Add synthetic date generation to telemetry_chargeback role in FVT repo
Generate synthetic data and write results to a json file Push json file to log directory so that during debug file is available for review Controller/ci-framework-data/tests/feature-verification-tests Do not overwrite syth data json if it already exists Using Gemini and Cursor Closes https://issues.redhat.com/browse/OSPRH-23746
1 parent 61b2ebb commit 6bd0bf0

File tree

6 files changed

+227
-1
lines changed

6 files changed

+227
-1
lines changed

ci/run_chargeback_tests.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
---
22
- name: "Verify all the applicable projects, endpoints, pods & services for cloudkitty"
33
hosts: "{{ cifmw_target_hook_host | default('localhost') }}"
4-
gather_facts: no
4+
gather_facts: true
55
ignore_errors: true
66
environment:
77
KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
88
PATH: "{{ cifmw_path }}"
99
vars_files:
10+
- vars/common.yml
1011
- vars/osp18_env.yml
1112
vars:
1213
common_pod_status_str: "Running"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
11
---
22
openstack_cmd: "openstack"
3+
4+
output_file_local: "{{ role_path }}/files/loki_synth_data.json"
5+
ck_py_script_path: "{{ role_path }}/files/gen_synth_loki_data.py"
6+
ck_data_template_path: "{{ role_path }}/files/loki_data_templ.j2"
7+
ck_days: 30
8+
ck_step: 300
9+
10+
# Output directory for test artifacts
11+
tests_dir: "{{ ansible_env.HOME }}/ci-framework-data/tests"
12+
logs_dir: "/home/zuul/ci-framework-data/tests/feature-verification-tests"
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import logging
2+
import argparse
3+
from datetime import datetime, timezone, timedelta
4+
from pathlib import Path
5+
from typing import Union
6+
from jinja2 import Template
7+
8+
# --- Configure logging with a default level that can be changed ---
9+
logging.basicConfig(
10+
level=logging.INFO,
11+
format='%(asctime)s - %(levelname)s - %(message)s',
12+
datefmt='%Y-%m-%d %H:%M:%S'
13+
)
14+
logger = logging.getLogger()
15+
16+
def _format_timestamp(epoch_seconds: float) -> str:
17+
"""
18+
Converts an epoch timestamp into a human-readable UTC string.
19+
20+
Args:
21+
epoch_seconds (float): The timestamp in seconds since the epoch.
22+
23+
Returns:
24+
str: The formatted datetime string (e.g., "2023-10-26T14:30:00 UTC").
25+
"""
26+
try:
27+
dt_object = datetime.fromtimestamp(epoch_seconds, tz=timezone.utc)
28+
return dt_object.strftime("%Y-%m-%dT%H:%M:%S %Z")
29+
except (ValueError, TypeError):
30+
logger.warning(f"Invalid epoch value provided: {epoch_seconds}")
31+
return "INVALID_TIMESTAMP"
32+
33+
def generate_loki_data(
34+
template_path: Path,
35+
output_path: Path,
36+
start_time: datetime,
37+
end_time: datetime,
38+
time_step_seconds: int
39+
):
40+
"""
41+
Generates synthetic Loki log data by first preparing a data list
42+
and then rendering it with a single template.
43+
44+
Args:
45+
template_path (Path): Path to the main log template file.
46+
output_path (Path): Path for the generated output JSON file.
47+
start_time (datetime): The start time for data generation.
48+
end_time (datetime): The end time for data generation.
49+
time_step_seconds (int): The duration of each log entry in seconds.
50+
"""
51+
52+
# --- Step 1: Generate the data structure first ---
53+
logger.info(
54+
f"Generating data from {start_time.strftime('%Y-%m-%d')} to "
55+
f"{end_time.strftime('%Y-%m-%d')} with a {time_step_seconds}s step."
56+
)
57+
start_epoch = int(start_time.timestamp())
58+
end_epoch = int(end_time.timestamp())
59+
logger.debug(f"Time range in epoch seconds: {start_epoch} to {end_epoch}")
60+
61+
log_data_list = [] # This list will hold all our data points
62+
63+
# Loop through the time range and generate data points
64+
for current_epoch in range(start_epoch, end_epoch, time_step_seconds):
65+
end_of_step_epoch = current_epoch + time_step_seconds - 1
66+
67+
# Prepare replacement values
68+
nanoseconds = int(current_epoch * 1_000_000_000)
69+
start_str = _format_timestamp(current_epoch)
70+
end_str = _format_timestamp(end_of_step_epoch)
71+
72+
logger.debug(f"Processing epoch: {current_epoch} -> nanoseconds: {nanoseconds}")
73+
74+
# Create a dictionary for this time step and add it to the list
75+
log_data_list.append({
76+
"nanoseconds": nanoseconds,
77+
"start_time": start_str,
78+
"end_time": end_str
79+
})
80+
81+
logger.info(f"Generated {len(log_data_list)} data points to be rendered.")
82+
83+
# --- Step 2: Load template and render ---
84+
try:
85+
logger.info(f"Loading main template from: {template_path}")
86+
template_content = template_path.read_text()
87+
template = Template(template_content, trim_blocks=True, lstrip_blocks=True)
88+
89+
except FileNotFoundError as e:
90+
logger.error(f"Error loading template file: {e}. Aborting.")
91+
raise # Re-raise the exception to be caught in main()
92+
93+
# --- Render the template in one pass with all the data ---
94+
logger.info("Rendering final output...")
95+
# The template expects a variable named 'log_data'
96+
final_output = template.render(log_data=log_data_list)
97+
98+
# --- Step 3: Write the final string to the file ---
99+
try:
100+
with output_path.open('w') as f_out:
101+
f_out.write(final_output)
102+
logger.info(f"Successfully generated synthetic data to '{output_path}'")
103+
except IOError as e:
104+
logger.error(f"Failed to write to output file '{output_path}': {e}")
105+
except Exception as e:
106+
logger.error(f"An unexpected error occurred during file write: {e}")
107+
108+
def main():
109+
"""Main entry point for the script."""
110+
parser = argparse.ArgumentParser(
111+
description="Generate synthetic Loki log data from a single main template.",
112+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
113+
)
114+
# --- Required File Path Arguments ---
115+
parser.add_argument("-o", "--output", required=True, help="Path to the output file.")
116+
# --- Only one template argument is needed now ---
117+
parser.add_argument("--template", required=True, help="Path to the main log template file (e.g., loki_main.tmpl).")
118+
119+
# --- Optional Generation Arguments ---
120+
parser.add_argument("--days", type=int, default=30, help="How many days of data to generate, ending today.")
121+
parser.add_argument("--step", type=int, default=300, help="Time step in seconds for each log entry.")
122+
123+
# --- Optional Utility Arguments ---
124+
parser.add_argument("--debug", action="store_true", help="Enable debug level logging for verbose output.")
125+
126+
args = parser.parse_args()
127+
128+
if args.debug:
129+
logger.setLevel(logging.DEBUG)
130+
logger.debug("Debug mode enabled.")
131+
132+
# Define the time range for data generation
133+
end_time_utc = datetime.now(timezone.utc)
134+
start_time_utc = end_time_utc - timedelta(days=args.days)
135+
logger.debug(f"Time range calculated: {start_time_utc} to {end_time_utc}")
136+
137+
# Run the generator
138+
try:
139+
generate_loki_data(
140+
template_path=Path(args.template),
141+
output_path=Path(args.output),
142+
start_time=start_time_utc,
143+
end_time=end_time_utc,
144+
time_step_seconds=args.step
145+
)
146+
except FileNotFoundError:
147+
logger.error("Process aborted because the template file was not found.")
148+
except Exception as e:
149+
logger.critical(f"A critical, unhandled error stopped the script: {e}")
150+
151+
152+
if __name__ == "__main__":
153+
main()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{"streams": [{ "stream": { "service": "cloudkitty" }, "values": [
2+
{%- for item in log_data %}
3+
[
4+
"{{ item.nanoseconds }}",
5+
"{\"start\": \"{{ item.start_time }}\", \"end\": \"{{ item.end_time }}\", \"type\": \"image.size\", \"unit\": \"MiB\", \"description\": null, \"qty\": 20.6875, \"price\": 0.0206875, \"groupby\": {\"id\": \"cd65d30f-8b94-4fa3-95dc-e3b429f479b2\", \"project_id\": \"0030775de80e4d84a4fd0d73e0a1b3a7\", \"user_id\": null, \"week_of_the_year\": \"37\", \"day_of_the_year\": \"258\", \"month\": \"9\", \"year\": \"2025\"}, \"metadata\": {\"container_format\": \"bare\", \"disk_format\": \"qcow2\"}}"
6+
],
7+
[
8+
"{{ item.nanoseconds }}",
9+
"{\"start\": \"{{ item.start_time }}\", \"end\": \"{{ item.end_time }}\", \"type\": \"instance\", \"unit\": \"instance\", \"description\": null, \"qty\": 1.0, \"price\": 0.3, \"groupby\": {\"id\": \"de168c31-ed44-4a1a-a079-51bd238a91d6\", \"project_id\": \"9cf5bcfc61a24682acc448af2d062ad2\", \"user_id\": \"c29ab6e886354bbd88ee9899e62d1d40\", \"week_of_the_year\": \"37\", \"day_of_the_year\": \"258\", \"month\": \"9\", \"year\": \"2025\"}, \"metadata\": {\"flavor_name\": \"m1.tiny\", \"flavor_id\": \"1\", \"vcpus\": \"\"}}"
10+
]
11+
{#- This logic adds a comma after every pair, *except* for the very last one. #}
12+
{%- if not loop.last -%}
13+
,
14+
{%- endif -%}
15+
{%- endfor %}
16+
]}]}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
- name: "Define Synthetic Data Variables"
2+
ansible.builtin.set_fact:
3+
output_file_remote: "{{ logs_dir }}/gen_loki_synth_data.log"
4+
5+
- name: Check for preexisting output file
6+
ansible.builtin.stat:
7+
path: "{{ output_file_local }}"
8+
register: file_preexists
9+
10+
- name: TEST Generate Synthetic Data
11+
ansible.builtin.command:
12+
cmd: >
13+
python3 "{{ ck_py_script_path }}"
14+
--template "{{ ck_data_template_path }}"
15+
-o "{{ output_file_local }}"
16+
--days "{{ ck_days }}"
17+
--step "{{ ck_step }}"
18+
register: script_output
19+
when: file_preexists.stat.exists is false
20+
changed_when: script_output.rc == 0
21+
22+
- name: Read the content of the file
23+
ansible.builtin.slurp:
24+
src: "{{ output_file_local }}"
25+
register: slurped_file
26+
27+
- name: TEST Validate JSON format of syntheticc data file
28+
ansible.builtin.assert:
29+
that:
30+
# This filter will trigger a task failure if the string isn't valid JSON
31+
- slurped_file.content | b64decode | from_json is defined
32+
fail_msg: "The file does not contain valid JSON format."
33+
success_msg: "JSON format validated successfully."
34+
35+
- name: Print output_file_remote path
36+
ansible.builtin.debug:
37+
msg: "Sythetic data file: {{ output_file_remote }}"
38+
39+
- name: Copy output file to remote host
40+
ansible.builtin.copy:
41+
src: "{{ output_file_local }}"
42+
dest: "{{ output_file_remote }}"
43+
mode: '0644'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
---
22
- name: "Validate Chargeback Feature"
33
ansible.builtin.include_tasks: "chargeback_tests.yml"
4+
5+
- name: "Generate Synthetic Data"
6+
ansible.builtin.include_tasks: "gen_synth_loki_data.yml"

0 commit comments

Comments
 (0)