Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions docs/GRAPHITE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may not be clear what is the data dictionary in this case. In the paragraph above, we talk about tags, so I suggest replacing this statement with The following tags are supported:


| 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:
Expand All @@ -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
```
15 changes: 7 additions & 8 deletions examples/graphite/datagen/datagen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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\"}
}"
}


Expand Down
6 changes: 3 additions & 3 deletions examples/graphite/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -68,7 +69,6 @@ services:
- otava-graphite
volumes:
- ./config:/config

networks:
otava-graphite:
driver: bridge
76 changes: 27 additions & 49 deletions otava/graphite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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__(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is something funky going on with indentation. Intelij highlights edits in this commit differently from the rest of the file:

Image Image

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],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idea (maybe outside of scope of this PR): today, if we have other event tags like "environment": "prod", we'll crash with unexpected keyword argument. Maybe we can change this class such that it'd work arbitrary tags.

):
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":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic was lost in this PR. If a string text is empty or equal to "null", we still want to treat it as empty, unless there is a reason not to.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change uses a different parsing logic than parse_datetime. The latter does dateparser.parse(date, settings={"RETURN_AS_TIMEZONE_AWARE": True}).

Why do need these extra manipulations with pub_time? I don't follow how making fields optional required it.



def compress_target_paths(paths: List[str]) -> List[str]:
Expand Down Expand Up @@ -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
Expand All @@ -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"
]
Expand All @@ -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:
Expand Down