From d3ad7c0ca4d3ed13641e0271c6b69deccf5716f7 Mon Sep 17 00:00:00 2001 From: sarthaksingh94 Date: Sun, 22 Feb 2026 10:32:45 -0600 Subject: [PATCH 1/2] Fix data generation script for tags Fix data generation script to send required fields nit remove non graphite changes nit Add datagen changes Fails still but can deploy local code Set defaults for all variables and manually specify pub_time Add table on supported metadata field nit Only confirm pub_time is set properly --- docs/GRAPHITE.md | 41 +++++++++------ examples/graphite/datagen/datagen.sh | 14 ++--- examples/graphite/docker-compose.yaml | 6 +-- otava/graphite.py | 76 ++++++++++----------------- 4 files changed, 63 insertions(+), 74 deletions(-) diff --git a/docs/GRAPHITE.md b/docs/GRAPHITE.md index 2f011293..aa526070 100644 --- a/docs/GRAPHITE.md +++ b/docs/GRAPHITE.md @@ -73,10 +73,6 @@ tests: ``` ### Tags - -> [!WARNING] -> Tags do not work as expected in the current version. See https://github.com/apache/otava/issues/24 for more details - The optional `tags` property contains the tags that are used to query for Graphite events that store additional test run metadata such as run identifier, commit, branch and product version information. @@ -94,6 +90,21 @@ $ curl -X POST "http://graphite_address/events/" \ Posting those events is not mandatory, but when they are available, Otava is able to filter data by commit or version using `--since-commit` or `--since-version` selectors. +#### Supported Metadata Schema +The following keys are supported within the `data` dictionary: + +| Field | Description | Default Value | +| :--- | :--- | :--- | +| **`test_owner`** | The user or team responsible for the run. | `"null"` | +| **`test_name`** | The name of the test suite executed. | `"null"` | +| **`run_id`** | Unique identifier for the specific run. | `"null"` | +| **`status`** | The outcome (e.g., `success`, `failure`). | `"null"` | +| **`version`** | The product version being tested. | `"null"` | +| **`branch`** | The VCS branch name. | `"null"` | +| **`commit`** | The specific commit hash. | `"null"` | +| **`start_time`** | Timestamp of test start. | `when` (from Graphite) | +| **`end_time`** | Timestamp of test end. | `when` (from Graphite) | + ## Example Start docker-compose with Graphite in one tab: @@ -111,15 +122,15 @@ docker-compose -f examples/graphite/docker-compose.yaml run --rm otava analyze m Expected output: ```bash -time run branch version commit throughput response_time cpu_usage -------------------------- ----- -------- --------- -------- ------------ --------------- ----------- -2024-12-14 22:45:10 +0000 61160 87 0.2 -2024-12-14 22:46:10 +0000 60160 85 0.3 -2024-12-14 22:47:10 +0000 60960 89 0.1 - ············ ··········· - -5.6% +300.0% - ············ ··········· -2024-12-14 22:48:10 +0000 57123 88 0.8 -2024-12-14 22:49:10 +0000 57980 87 0.9 -2024-12-14 22:50:10 +0000 56950 85 0.7 +time run branch version commit throughput response_time cpu_usage +------------------------- ----- ----------- --------- -------- ------------ --------------- ----------- +2026-02-22 18:51:10 +0000 null new-feature 0.0.1 p7q8r9 61160 87 0.2 +2026-02-22 18:52:10 +0000 null new-feature 0.0.1 m4n5o6 60160 85 0.3 +2026-02-22 18:53:10 +0000 null new-feature 0.0.1 j1k2l3 60960 89 0.1 + ············ ··········· + -5.6% +300.0% + ············ ··········· +2026-02-22 18:54:10 +0000 null new-feature 0.0.1 g7h8i9 57123 88 0.8 +2026-02-22 18:55:10 +0000 null new-feature 0.0.1 d4e5f6 57980 87 0.9 +2026-02-22 18:56:10 +0000 null new-feature 0.0.1 a1b2c3 56950 85 0.7 ``` diff --git a/examples/graphite/datagen/datagen.sh b/examples/graphite/datagen/datagen.sh index 8217ceb0..dea09d76 100755 --- a/examples/graphite/datagen/datagen.sh +++ b/examples/graphite/datagen/datagen.sh @@ -43,13 +43,13 @@ send_to_graphite() { echo "${throughput_path} ${value} ${timestamp}" | nc ${GRAPHITE_SERVER} ${GRAPHITE_PORT} # annotate the metric # Commented out, waiting for https://github.com/apache/otava/issues/24 to be fixed - # curl -X POST "http://${GRAPHITE_SERVER}/events/" \ - # -d "{ - # \"what\": \"Performance Test\", - # \"tags\": [\"perf-test\", \"daily\", \"my-product\"], - # \"when\": ${timestamp}, - # \"data\": {\"commit\": \"${commit}\", \"branch\": \"new-feature\", \"version\": \"0.0.1\"} - # }" + curl -X POST "http://${GRAPHITE_SERVER}/events/" \ + -d "{ + \"what\": \"Performance Test\", + \"tags\": [\"perf-test\", \"daily\", \"my-product\"], + \"when\": ${timestamp}, + \"data\": {\"commit\": \"${commit}\", \"branch\": \"new-feature\", \"version\": \"0.0.1\"} + }" } diff --git a/examples/graphite/docker-compose.yaml b/examples/graphite/docker-compose.yaml index ea3c107e..2e2d2143 100644 --- a/examples/graphite/docker-compose.yaml +++ b/examples/graphite/docker-compose.yaml @@ -43,13 +43,14 @@ services: - otava-graphite data-sender: - image: bash + image: alpine:latest container_name: data-sender depends_on: - graphite volumes: - ./datagen:/datagen - entrypoint: ["bash", "/datagen/datagen.sh"] + # Install bash and curl before running the script + entrypoint: ["/bin/sh", "-c", "apk add --no-cache bash curl && bash /datagen/datagen.sh"] networks: - otava-graphite @@ -68,7 +69,6 @@ services: - otava-graphite volumes: - ./config:/config - networks: otava-graphite: driver: bridge diff --git a/otava/graphite.py b/otava/graphite.py index 69a7592e..73fbd3da 100644 --- a/otava/graphite.py +++ b/otava/graphite.py @@ -57,7 +57,6 @@ class TimeSeries: def decode_graphite_datapoints(series: Dict[str, List[List[float]]]) -> List[DataPoint]: - points = series["datapoints"] return [DataPoint(int(p[1]), p[0]) for p in points if p[0] is not None] @@ -80,49 +79,28 @@ class GraphiteError(IOError): @dataclass class GraphiteEvent: - test_owner: str - test_name: str - run_id: str - status: str - start_time: datetime pub_time: datetime - end_time: datetime - version: Optional[str] - branch: Optional[str] - commit: Optional[str] - - def __init__( - self, - pub_time: int, - test_owner: str, - test_name: str, - run_id: str, - status: str, - start_time: int, - end_time: int, - version: Optional[str], - branch: Optional[str], - commit: Optional[str], - ): - self.test_owner = test_owner - self.test_name = test_name - self.run_id = run_id - self.status = status - self.start_time = parse_datetime(str(start_time)) - self.pub_time = parse_datetime(str(pub_time)) - self.end_time = parse_datetime(str(end_time)) - if len(version) == 0 or version == "null": - self.version = None - else: - self.version = version - if len(branch) == 0 or branch == "null": - self.branch = None - else: - self.branch = branch - if len(commit) == 0 or commit == "null": - self.commit = None - else: - self.commit = commit + test_owner: Optional[str] = "null" + test_name: Optional[str] = "null" + run_id: Optional[str] = "null" + status: Optional[str] = "null" + start_time: Optional[datetime] = None + end_time: Optional[datetime] = None + version: Optional[str] = "null" + branch: Optional[str] = "null" + commit: Optional[str] = "null" + + + def __post_init__(self): + if self.pub_time is None: + raise ValueError("pub_time is required and cannot be None") + # Ensure pub_time is always a datetime + + # Only parse if it isn't already a datetime object + if isinstance(self.pub_time, str): + self.pub_time = parse_datetime(self.pub_time) + elif isinstance(self.pub_time, (int, float)): + self.pub_time = datetime.fromtimestamp(self.pub_time) def compress_target_paths(paths: List[str]) -> List[str]: @@ -160,10 +138,10 @@ def __init__(self, conf: GraphiteConfig): self.__url_limit = 4094 def fetch_events( - self, - tags: Iterable[str], - from_time: Optional[datetime] = None, - until_time: Optional[datetime] = None, + self, + tags: Iterable[str], + from_time: Optional[datetime] = None, + until_time: Optional[datetime] = None, ) -> List[GraphiteEvent]: """ Returns 'Performance Test' events that match all of @@ -190,7 +168,7 @@ def fetch_events( data_str = urllib.request.urlopen(url).read() data_as_json = json.loads(data_str) return [ - GraphiteEvent(event.get("when"), **ast.literal_eval(event.get("data"))) + GraphiteEvent(pub_time=event.get("when"), **ast.literal_eval(event.get("data"))) for event in data_as_json if event.get("what") == "Performance Test" ] @@ -199,7 +177,7 @@ def fetch_events( raise GraphiteError(f"Failed to fetch Graphite events: {str(e)}") def fetch_events_with_matching_time_option( - self, tags: Iterable[str], commit: Optional[str], version: Optional[str] + self, tags: Iterable[str], commit: Optional[str], version: Optional[str] ) -> List[GraphiteEvent]: events = [] if commit is not None: From adc9f029aacb9b24f0e923b6eed6ef7cd284b65e Mon Sep 17 00:00:00 2001 From: sarthaksingh94 Date: Mon, 23 Feb 2026 22:35:13 -0600 Subject: [PATCH 2/2] make defaults none documentation fixup --- docs/GRAPHITE.md | 22 +++++++++++----------- examples/graphite/datagen/datagen.sh | 1 - otava/graphite.py | 14 +++++++------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/docs/GRAPHITE.md b/docs/GRAPHITE.md index aa526070..44e8385d 100644 --- a/docs/GRAPHITE.md +++ b/docs/GRAPHITE.md @@ -93,17 +93,17 @@ filter data by commit or version using `--since-commit` or `--since-version` sel #### Supported Metadata Schema The following keys are supported within the `data` dictionary: -| Field | Description | Default Value | -| :--- | :--- | :--- | -| **`test_owner`** | The user or team responsible for the run. | `"null"` | -| **`test_name`** | The name of the test suite executed. | `"null"` | -| **`run_id`** | Unique identifier for the specific run. | `"null"` | -| **`status`** | The outcome (e.g., `success`, `failure`). | `"null"` | -| **`version`** | The product version being tested. | `"null"` | -| **`branch`** | The VCS branch name. | `"null"` | -| **`commit`** | The specific commit hash. | `"null"` | -| **`start_time`** | Timestamp of test start. | `when` (from Graphite) | -| **`end_time`** | Timestamp of test end. | `when` (from Graphite) | +| Field | Description | Default Value | +| :--- | :--- |:-----------------------| +| **`test_owner`** | The user or team responsible for the run. | `None` | +| **`test_name`** | The name of the test suite executed. | `None` | +| **`run_id`** | Unique identifier for the specific run. | `None` | +| **`status`** | The outcome (e.g., `success`, `failure`). | `None` | +| **`version`** | The product version being tested. | `None` | +| **`branch`** | The VCS branch name. | `None` | +| **`commit`** | The specific commit hash. | `None` | +| **`start_time`** | Timestamp of test start. | `None` | +| **`end_time`** | Timestamp of test end. | `None` | ## Example diff --git a/examples/graphite/datagen/datagen.sh b/examples/graphite/datagen/datagen.sh index dea09d76..b9e8b5dd 100755 --- a/examples/graphite/datagen/datagen.sh +++ b/examples/graphite/datagen/datagen.sh @@ -42,7 +42,6 @@ send_to_graphite() { # send the metric echo "${throughput_path} ${value} ${timestamp}" | nc ${GRAPHITE_SERVER} ${GRAPHITE_PORT} # annotate the metric - # Commented out, waiting for https://github.com/apache/otava/issues/24 to be fixed curl -X POST "http://${GRAPHITE_SERVER}/events/" \ -d "{ \"what\": \"Performance Test\", diff --git a/otava/graphite.py b/otava/graphite.py index 73fbd3da..228f4b2e 100644 --- a/otava/graphite.py +++ b/otava/graphite.py @@ -80,15 +80,15 @@ class GraphiteError(IOError): @dataclass class GraphiteEvent: pub_time: datetime - test_owner: Optional[str] = "null" - test_name: Optional[str] = "null" - run_id: Optional[str] = "null" - status: Optional[str] = "null" + test_owner: Optional[str] = None + test_name: Optional[str] = None + run_id: Optional[str] = None + status: Optional[str] = None start_time: Optional[datetime] = None end_time: Optional[datetime] = None - version: Optional[str] = "null" - branch: Optional[str] = "null" - commit: Optional[str] = "null" + version: Optional[str] = None + branch: Optional[str] = None + commit: Optional[str] = None def __post_init__(self):