Skip to content

Commit 36ac68f

Browse files
committed
Updating data structure sent to Log Detective API
Improving code snippet annotations Signed-off-by: Jiri Podivin <jpodivin@redhat.com>
1 parent 9191f01 commit 36ac68f

5 files changed

Lines changed: 96 additions & 18 deletions

File tree

README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Should the analysis fail, for whatever reason, an error is posted to the same
1515

1616
The endpoint expects a JSON payload matching the `BuildInfo` model:
1717

18-
```
18+
```json
1919
{
2020
"artifacts": {
2121
"builder-live.log": "http://example.com/builder-live.log",
@@ -25,11 +25,15 @@ The endpoint expects a JSON payload matching the `BuildInfo` model:
2525
"build_system": "copr",
2626
"commit_sha": "9deb98c730bb4123f518ca13a0dbec5d7c0669ca",
2727
"project_url": "www.logdetective.com",
28-
"pr_id": 1
28+
"pr_id": 1,
29+
"build_metadata": {
30+
"commentary": "I've made a terrible mistake",
31+
"infra_status": "BROKEN"
32+
}
2933
}
3034
```
3135

32-
artifacts (dict): A dictionary mapping log filenames to their full URL.
36+
artifacts (dict): A dictionary mapping log filenames to their full URL or raw contents.
3337

3438
target_build (str): A unique identifier for the build, which will be included in the message.
3539

@@ -41,7 +45,17 @@ project_url (str, optional): URL of the project the build is for
4145

4246
pr_id (int, optional): Identifier of the pull request, or equivalent
4347

44-
Of these values, only `artifacts` are used by Log Detective itself.
48+
build_metadata: (dict, optional): Dictionary of additional information about concluded build:
49+
50+
specfile (str, optional): Contents of package spec file
51+
52+
last_patch (str, optional): The last patch applied as a string
53+
54+
commentary (str, optional): Additional relevant information, such as PR description
55+
56+
infra_status (str, optional): Infrastructure status
57+
58+
Of these values, only `artifacts` and `build_metadata` are used by Log Detective itself.
4559
The rest is used as part of a message sent to Fedora Messaging infrastructure,
4660
to identify results.
4761

@@ -58,13 +72,13 @@ Additionally, for Sentry error and performance monitoring, `LD_PACKIT_INTERFACE_
5872
Images are published to quay.io. If it isn't available, or if you want
5973
to test your own changes. First build your own image
6074

61-
```
75+
```bash
6276
podman build -t logdetective-packit .
6377
```
6478

6579
and then run the container:
6680

67-
```
81+
```bash
6882
podman run -d --name logdetective-packit \
6983
-p 8090:8090 \
7084
-e LD_URL="https://logdetective.example.com/api" \
@@ -108,6 +122,6 @@ When using uv, this is handled automatically via `[tool.uv.sources]` in pyprojec
108122

109123
Tests should be executed with `uv`:
110124

111-
```
125+
```bash
112126
uv run pytest
113127
```

src/logdetective_packit/main.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from logdetective_packit_message import LogDetectiveResult, LogDetectiveMessage
2525

2626
from logdetective_packit.models import BuildInfo, Response
27+
from logdetective_packit.utils import is_url
2728

2829
LD_URL = os.environ.get("LD_URL")
2930
LD_TOKEN = os.environ.get("LD_TOKEN", "")
@@ -98,11 +99,27 @@ async def call_log_detective(
9899
log_detective_analysis_id: str,
99100
log_detective_analysis_start: datetime,
100101
) -> None:
101-
"""Analyze build artifacts using Log Detective API. Only the first log
102-
is analyzed."""
103-
build_artifacts = list(build_info.artifacts.items())
104-
log_url = build_artifacts[0][1]
102+
"""Analyze build artifacts using Log Detective API."""
103+
105104
headers = {}
105+
files = []
106+
analysis_request = {}
107+
108+
for artifact_identity, artifact_content in build_info.artifacts.items():
109+
if is_url(artifact_content):
110+
files.append({"name": artifact_identity, "url": artifact_content})
111+
else:
112+
files.append(
113+
{
114+
"name": artifact_identity,
115+
"content": artifact_content,
116+
}
117+
)
118+
119+
analysis_request["files"] = files
120+
121+
if build_info.build_metadata:
122+
analysis_request["build_metadata"] = build_info.build_metadata.model_dump()
106123

107124
# If Log Detective server requires authorization
108125
if LD_TOKEN:
@@ -111,7 +128,7 @@ async def call_log_detective(
111128
response = await http_client.post(
112129
url=LD_URL,
113130
headers=headers,
114-
json={"url": log_url},
131+
json=analysis_request,
115132
)
116133
response.raise_for_status()
117134
except HTTPStatusError as ex:

src/logdetective_packit/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,24 @@
33
from pydantic import BaseModel, Field
44

55

6+
class BuildMetadata(BaseModel):
7+
"""Model of additional information provided about the build."""
8+
9+
specfile: Optional[str] = Field(
10+
description="Contents of package spec file as a string.", default=None
11+
)
12+
last_patch: Optional[str] = Field(
13+
description="Contents of last patch applied as a string.", default=None
14+
)
15+
commentary: Optional[str] = Field(
16+
description="Comment attached to the triggered build, such as PR description.",
17+
default=None,
18+
)
19+
infra_status: Optional[str] = Field(
20+
description="State of build infrastructure as a string.", default=None
21+
)
22+
23+
624
class BuildInfo(BaseModel):
725
"""ID of the build being analyzed and URL to and all artifacts."""
826

@@ -24,6 +42,9 @@ class BuildInfo(BaseModel):
2442
description="ID of the pull request, or equivalent of the given forge",
2543
default=None,
2644
)
45+
build_metadata: Optional[BuildMetadata] = Field(
46+
description="Optional build metadata.", default=None
47+
)
2748

2849

2950
class Response(BaseModel):

src/logdetective_packit/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from urllib.parse import urlparse
2+
3+
4+
def is_url(url):
5+
try:
6+
result = urlparse(url)
7+
return all([result.scheme, result.netloc])
8+
except ValueError:
9+
return False

tests/test_main.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
publish_message,
1919
PUBLISH_TIMEOUT,
2020
)
21+
from logdetective_packit.utils import is_url
2122

2223

2324
from tests.utils import (
@@ -117,9 +118,10 @@ async def test_publish_message_exceptions(
117118
async def test_call_log_detective(
118119
mock_env_vars, mock_external_calls, mock_server_logger
119120
):
120-
121121
log_detective_analysis_id = "8052517e-cf69-11f0-9b27-9a478821d0e2"
122-
log_detective_build_analysis_start = datetime.fromisoformat("2025-12-10 10:57:57.341695+00:00")
122+
log_detective_build_analysis_start = datetime.fromisoformat(
123+
"2025-12-10 10:57:57.341695+00:00"
124+
)
123125
build_info = BuildInfo(**MINIMAL_BUILD_INFO)
124126
await call_log_detective(
125127
build_info=build_info,
@@ -144,7 +146,9 @@ async def test_call_log_detective_request_exception(
144146

145147
with pytest.raises(HTTPStatusError):
146148
log_detective_build_analysis_id = "8052517e-cf69-11f0-9b27-9a478821d0e2"
147-
log_detective_build_analysis_start = datetime.fromisoformat("2025-12-10 10:57:57.341695+00:00")
149+
log_detective_build_analysis_start = datetime.fromisoformat(
150+
"2025-12-10 10:57:57.341695+00:00"
151+
)
148152
await call_log_detective(
149153
build_info=build_info,
150154
log_detective_analysis_id=log_detective_build_analysis_id,
@@ -160,13 +164,18 @@ async def test_analyze_build_skeleton(
160164
):
161165
"""Test for the entire /analyze endpoint."""
162166

167+
# Mock is_url to test calls
168+
mock_is_url = mocker.patch("logdetective_packit.main.is_url", side_effect=is_url)
169+
163170
# Mock the return value of requests.post().json()
164171
mock_response = mocker.Mock()
165172
mock_response.json.return_value = {"status": "analysis_started", "id": "fake-id"}
166173
mock_response.raise_for_status = mocker.Mock()
167174
mock_external_calls["mock_async_client"].post.return_value = mock_response
168175

169-
monkeypatch.setattr("logdetective_packit.main.LD_URL", "http://mock-ld-server.com/api")
176+
monkeypatch.setattr(
177+
"logdetective_packit.main.LD_URL", "http://mock-ld-server.com/api"
178+
)
170179
monkeypatch.setattr("logdetective_packit.main.LD_TOKEN", "test-token-123")
171180
monkeypatch.setattr("logdetective_packit.main.LD_PACKIT_TOKEN", "secret-123")
172181

@@ -193,8 +202,16 @@ async def test_analyze_build_skeleton(
193202
# Check that requests.post was called correctly
194203
expected_headers = {"Authorization": "Bearer test-token-123"}
195204
# The code only takes the first log URL
196-
expected_data = {"url": "http://example.com/builder-live.log"}
197-
205+
expected_data = {
206+
"files": [
207+
{
208+
"name": "builder-live.log",
209+
"url": "http://example.com/builder-live.log",
210+
}
211+
]
212+
}
213+
214+
mock_is_url.assert_called_once_with("http://example.com/builder-live.log")
198215
mock_external_calls["mock_async_client"].post.assert_called_once_with(
199216
url="http://mock-ld-server.com/api",
200217
json=expected_data,

0 commit comments

Comments
 (0)