diff --git a/docs/GRAPHITE.md b/docs/GRAPHITE.md index 2f011293..44e8385d 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. | `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 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..b9e8b5dd 100755 --- a/examples/graphite/datagen/datagen.sh +++ b/examples/graphite/datagen/datagen.sh @@ -42,14 +42,13 @@ 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\", - # \"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..228f4b2e 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] = 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] = None + branch: Optional[str] = None + commit: Optional[str] = None + + + 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: