logdetective 0.5.10__py3-none-any.whl → 0.5.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- logdetective/constants.py +8 -0
- logdetective/logdetective.py +8 -1
- logdetective/prompts.yml +6 -0
- logdetective/server/database/models.py +216 -12
- logdetective/server/metric.py +4 -6
- logdetective/server/models.py +11 -3
- logdetective/server/plot.py +114 -39
- logdetective/server/server.py +104 -11
- logdetective/server/templates/{gitlab_comment.md.j2 → gitlab_full_comment.md.j2} +1 -3
- logdetective/server/templates/gitlab_short_comment.md.j2 +53 -0
- logdetective/server/utils.py +3 -1
- logdetective/utils.py +7 -3
- {logdetective-0.5.10.dist-info → logdetective-0.5.11.dist-info}/METADATA +28 -4
- logdetective-0.5.11.dist-info/RECORD +24 -0
- logdetective-0.5.10.dist-info/RECORD +0 -23
- {logdetective-0.5.10.dist-info → logdetective-0.5.11.dist-info}/LICENSE +0 -0
- {logdetective-0.5.10.dist-info → logdetective-0.5.11.dist-info}/WHEEL +0 -0
- {logdetective-0.5.10.dist-info → logdetective-0.5.11.dist-info}/entry_points.txt +0 -0
logdetective/constants.py
CHANGED
|
@@ -16,6 +16,8 @@ Snippets are delimited with '================'.
|
|
|
16
16
|
|
|
17
17
|
Finally, drawing on information from all snippets, provide complete explanation of the issue and recommend solution.
|
|
18
18
|
|
|
19
|
+
Explanation of the issue, and recommended solution, should take handful of sentences.
|
|
20
|
+
|
|
19
21
|
Snippets:
|
|
20
22
|
|
|
21
23
|
{}
|
|
@@ -38,6 +40,8 @@ Answer:
|
|
|
38
40
|
SNIPPET_PROMPT_TEMPLATE = """
|
|
39
41
|
Analyse following RPM build log snippet. Describe contents accurately, without speculation or suggestions for resolution.
|
|
40
42
|
|
|
43
|
+
Your analysis must be as concise as possible, while keeping relevant information intact.
|
|
44
|
+
|
|
41
45
|
Snippet:
|
|
42
46
|
|
|
43
47
|
{}
|
|
@@ -55,6 +59,8 @@ Snippets are delimited with '================'.
|
|
|
55
59
|
|
|
56
60
|
Drawing on information from all snippets, provide complete explanation of the issue and recommend solution.
|
|
57
61
|
|
|
62
|
+
Explanation of the issue, and recommended solution, should take handful of sentences.
|
|
63
|
+
|
|
58
64
|
Snippets:
|
|
59
65
|
|
|
60
66
|
{}
|
|
@@ -64,3 +70,5 @@ Analysis:
|
|
|
64
70
|
"""
|
|
65
71
|
|
|
66
72
|
SNIPPET_DELIMITER = "================"
|
|
73
|
+
|
|
74
|
+
DEFAULT_TEMPERATURE = 0.8
|
logdetective/logdetective.py
CHANGED
|
@@ -3,7 +3,7 @@ import logging
|
|
|
3
3
|
import sys
|
|
4
4
|
import os
|
|
5
5
|
|
|
6
|
-
from logdetective.constants import DEFAULT_ADVISOR
|
|
6
|
+
from logdetective.constants import DEFAULT_ADVISOR, DEFAULT_TEMPERATURE
|
|
7
7
|
from logdetective.utils import (
|
|
8
8
|
process_log,
|
|
9
9
|
initialize_model,
|
|
@@ -73,6 +73,12 @@ def setup_args():
|
|
|
73
73
|
default=f"{os.path.dirname(__file__)}/prompts.yml",
|
|
74
74
|
help="Path to prompt configuration file."
|
|
75
75
|
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--temperature",
|
|
78
|
+
type=float,
|
|
79
|
+
default=DEFAULT_TEMPERATURE,
|
|
80
|
+
help="Temperature for inference."
|
|
81
|
+
)
|
|
76
82
|
return parser.parse_args()
|
|
77
83
|
|
|
78
84
|
|
|
@@ -147,6 +153,7 @@ def main(): # pylint: disable=too-many-statements,too-many-locals
|
|
|
147
153
|
model,
|
|
148
154
|
stream,
|
|
149
155
|
prompt_template=prompts_configuration.prompt_template,
|
|
156
|
+
temperature=args.temperature,
|
|
150
157
|
)
|
|
151
158
|
probs = []
|
|
152
159
|
print("Explanation:")
|
logdetective/prompts.yml
CHANGED
|
@@ -13,6 +13,8 @@ prompt_template: |
|
|
|
13
13
|
|
|
14
14
|
Finally, drawing on information from all snippets, provide complete explanation of the issue and recommend solution.
|
|
15
15
|
|
|
16
|
+
Explanation of the issue, and recommended solution, should take handful of sentences.
|
|
17
|
+
|
|
16
18
|
Snippets:
|
|
17
19
|
|
|
18
20
|
{}
|
|
@@ -33,6 +35,8 @@ summarization_prompt_template: |
|
|
|
33
35
|
snippet_prompt_template: |
|
|
34
36
|
Analyse following RPM build log snippet. Describe contents accurately, without speculation or suggestions for resolution.
|
|
35
37
|
|
|
38
|
+
Your analysis must be as concise as possible, while keeping relevant information intact.
|
|
39
|
+
|
|
36
40
|
Snippet:
|
|
37
41
|
|
|
38
42
|
{}
|
|
@@ -48,6 +52,8 @@ prompt_template_staged: |
|
|
|
48
52
|
|
|
49
53
|
Drawing on information from all snippets, provide complete explanation of the issue and recommend solution.
|
|
50
54
|
|
|
55
|
+
Explanation of the issue, and recommended solution, should take handful of sentences.
|
|
56
|
+
|
|
51
57
|
Snippets:
|
|
52
58
|
|
|
53
59
|
{}
|
|
@@ -97,17 +97,35 @@ class AnalyzeRequestMetrics(Base):
|
|
|
97
97
|
metrics.response_certainty = response_certainty
|
|
98
98
|
session.add(metrics)
|
|
99
99
|
|
|
100
|
+
@classmethod
|
|
101
|
+
def get_postgres_time_format(cls, time_format):
|
|
102
|
+
"""Map python time format in the PostgreSQL format."""
|
|
103
|
+
if time_format == "%Y-%m-%d":
|
|
104
|
+
pgsql_time_format = "YYYY-MM-DD"
|
|
105
|
+
else:
|
|
106
|
+
pgsql_time_format = "YYYY-MM-DD HH24"
|
|
107
|
+
return pgsql_time_format
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def get_dictionary_with_datetime_keys(
|
|
111
|
+
cls, time_format: str, values_dict: dict[str, any]
|
|
112
|
+
) -> dict[datetime.datetime, any]:
|
|
113
|
+
"""Convert from a dictionary with str keys to a dictionary with datetime keys"""
|
|
114
|
+
new_dict = {
|
|
115
|
+
datetime.datetime.strptime(r[0], time_format): r[1] for r in values_dict
|
|
116
|
+
}
|
|
117
|
+
return new_dict
|
|
118
|
+
|
|
100
119
|
@classmethod
|
|
101
120
|
def _get_requests_by_time_for_postgres(
|
|
102
121
|
cls, start_time, end_time, time_format, endpoint
|
|
103
122
|
):
|
|
104
|
-
"""
|
|
123
|
+
"""Get total requests number in time period.
|
|
124
|
+
|
|
125
|
+
func.to_char is PostgreSQL specific.
|
|
105
126
|
Let's unit tests replace this function with the SQLite version.
|
|
106
127
|
"""
|
|
107
|
-
|
|
108
|
-
pgsql_time_format = "YYYY-MM-DD"
|
|
109
|
-
else:
|
|
110
|
-
pgsql_time_format = "YYYY-MM-DD HH24"
|
|
128
|
+
pgsql_time_format = cls.get_postgres_time_format(time_format)
|
|
111
129
|
|
|
112
130
|
requests_by_time_format = (
|
|
113
131
|
select(
|
|
@@ -123,10 +141,12 @@ class AnalyzeRequestMetrics(Base):
|
|
|
123
141
|
return requests_by_time_format
|
|
124
142
|
|
|
125
143
|
@classmethod
|
|
126
|
-
def
|
|
144
|
+
def _get_requests_by_time_for_sqlite(
|
|
127
145
|
cls, start_time, end_time, time_format, endpoint
|
|
128
146
|
):
|
|
129
|
-
"""
|
|
147
|
+
"""Get total requests number in time period.
|
|
148
|
+
|
|
149
|
+
func.strftime is SQLite specific.
|
|
130
150
|
Use this function in unit test using flexmock:
|
|
131
151
|
|
|
132
152
|
flexmock(AnalyzeRequestMetrics).should_receive("_get_requests_by_time_for_postgres")
|
|
@@ -178,9 +198,193 @@ class AnalyzeRequestMetrics(Base):
|
|
|
178
198
|
counts = session.execute(count_requests_by_time_format)
|
|
179
199
|
results = counts.fetchall()
|
|
180
200
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
201
|
+
return cls.get_dictionary_with_datetime_keys(time_format, results)
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def _get_average_responses_times_for_postgres(
|
|
205
|
+
cls, start_time, end_time, time_format, endpoint
|
|
206
|
+
):
|
|
207
|
+
"""Get average responses time.
|
|
208
|
+
|
|
209
|
+
func.to_char is PostgreSQL specific.
|
|
210
|
+
Let's unit tests replace this function with the SQLite version.
|
|
211
|
+
"""
|
|
212
|
+
with transaction(commit=False) as session:
|
|
213
|
+
pgsql_time_format = cls.get_postgres_time_format(time_format)
|
|
214
|
+
|
|
215
|
+
average_responses_times = (
|
|
216
|
+
select(
|
|
217
|
+
func.to_char(cls.request_received_at, pgsql_time_format).label(
|
|
218
|
+
"time_range"
|
|
219
|
+
),
|
|
220
|
+
(
|
|
221
|
+
func.avg(
|
|
222
|
+
func.extract( # pylint: disable=not-callable
|
|
223
|
+
"epoch", cls.response_sent_at - cls.request_received_at
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
).label("average_response_seconds"),
|
|
227
|
+
)
|
|
228
|
+
.filter(cls.request_received_at.between(start_time, end_time))
|
|
229
|
+
.filter(cls.endpoint == endpoint)
|
|
230
|
+
.group_by("time_range")
|
|
231
|
+
.order_by("time_range")
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
results = session.execute(average_responses_times).fetchall()
|
|
235
|
+
return results
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def _get_average_responses_times_for_sqlite(
|
|
239
|
+
cls, start_time, end_time, time_format, endpoint
|
|
240
|
+
):
|
|
241
|
+
"""Get average responses time.
|
|
242
|
+
|
|
243
|
+
func.strftime is SQLite specific.
|
|
244
|
+
Use this function in unit test using flexmock:
|
|
245
|
+
|
|
246
|
+
flexmock(AnalyzeRequestMetrics).should_receive("_get_average_responses_times_for_postgres")
|
|
247
|
+
.replace_with(AnalyzeRequestMetrics._get_average_responses_times_for_sqlite)
|
|
248
|
+
"""
|
|
249
|
+
with transaction(commit=False) as session:
|
|
250
|
+
average_responses_times = (
|
|
251
|
+
select(
|
|
252
|
+
func.strftime(time_format, cls.request_received_at).label(
|
|
253
|
+
"time_range"
|
|
254
|
+
),
|
|
255
|
+
(
|
|
256
|
+
func.avg(
|
|
257
|
+
func.julianday(cls.response_sent_at)
|
|
258
|
+
- func.julianday(cls.request_received_at) # noqa: W503 flake8 vs ruff
|
|
259
|
+
)
|
|
260
|
+
* 86400 # noqa: W503 flake8 vs ruff
|
|
261
|
+
).label("average_response_seconds"),
|
|
262
|
+
)
|
|
263
|
+
.filter(cls.request_received_at.between(start_time, end_time))
|
|
264
|
+
.filter(cls.endpoint == endpoint)
|
|
265
|
+
.group_by("time_range")
|
|
266
|
+
.order_by("time_range")
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
results = session.execute(average_responses_times).fetchall()
|
|
270
|
+
return results
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
def get_responses_average_time_in_period(
|
|
274
|
+
cls,
|
|
275
|
+
start_time: datetime.datetime,
|
|
276
|
+
end_time: datetime.datetime,
|
|
277
|
+
time_format: str,
|
|
278
|
+
endpoint: Optional[EndpointType] = EndpointType.ANALYZE,
|
|
279
|
+
) -> dict[datetime.datetime, int]:
|
|
280
|
+
"""
|
|
281
|
+
Get a dictionary with average responses times
|
|
282
|
+
grouped by time units within a specified period.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
start_time (datetime): The start of the time period to query
|
|
286
|
+
end_time (datetime): The end of the time period to query
|
|
287
|
+
time_format (str): The strftime format string to format timestamps (e.g., '%Y-%m-%d')
|
|
288
|
+
endpoint (EndpointType): The analyze API endpoint to query
|
|
185
289
|
|
|
186
|
-
|
|
290
|
+
Returns:
|
|
291
|
+
dict[datetime, int]: A dictionary mapping datetime objects
|
|
292
|
+
to average responses times
|
|
293
|
+
"""
|
|
294
|
+
with transaction(commit=False) as _:
|
|
295
|
+
average_responses_times = cls._get_average_responses_times_for_postgres(
|
|
296
|
+
start_time, end_time, time_format, endpoint
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return cls.get_dictionary_with_datetime_keys(
|
|
300
|
+
time_format, average_responses_times
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
@classmethod
|
|
304
|
+
def _get_average_responses_lengths_for_postgres(
|
|
305
|
+
cls, start_time, end_time, time_format, endpoint
|
|
306
|
+
):
|
|
307
|
+
"""Get average responses length.
|
|
308
|
+
|
|
309
|
+
func.to_char is PostgreSQL specific.
|
|
310
|
+
Let's unit tests replace this function with the SQLite version.
|
|
311
|
+
"""
|
|
312
|
+
with transaction(commit=False) as session:
|
|
313
|
+
pgsql_time_format = cls.get_postgres_time_format(time_format)
|
|
314
|
+
|
|
315
|
+
average_responses_lengths = (
|
|
316
|
+
select(
|
|
317
|
+
func.to_char(cls.request_received_at, pgsql_time_format).label(
|
|
318
|
+
"time_range"
|
|
319
|
+
),
|
|
320
|
+
(func.avg(cls.response_length)).label("average_responses_length"),
|
|
321
|
+
)
|
|
322
|
+
.filter(cls.request_received_at.between(start_time, end_time))
|
|
323
|
+
.filter(cls.endpoint == endpoint)
|
|
324
|
+
.group_by("time_range")
|
|
325
|
+
.order_by("time_range")
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
results = session.execute(average_responses_lengths).fetchall()
|
|
329
|
+
return results
|
|
330
|
+
|
|
331
|
+
@classmethod
|
|
332
|
+
def _get_average_responses_lengths_for_sqlite(
|
|
333
|
+
cls, start_time, end_time, time_format, endpoint
|
|
334
|
+
):
|
|
335
|
+
"""Get average responses length.
|
|
336
|
+
|
|
337
|
+
func.strftime is SQLite specific.
|
|
338
|
+
Use this function in unit test using flexmock:
|
|
339
|
+
|
|
340
|
+
flexmock(AnalyzeRequestMetrics)
|
|
341
|
+
.should_receive("_get_average_responses_lengths_for_postgres")
|
|
342
|
+
.replace_with(AnalyzeRequestMetrics._get_average_responses_lengths_for_sqlite)
|
|
343
|
+
"""
|
|
344
|
+
with transaction(commit=False) as session:
|
|
345
|
+
average_responses_lengths = (
|
|
346
|
+
select(
|
|
347
|
+
func.strftime(time_format, cls.request_received_at).label(
|
|
348
|
+
"time_range"
|
|
349
|
+
),
|
|
350
|
+
(func.avg(cls.response_length)).label("average_responses_length"),
|
|
351
|
+
)
|
|
352
|
+
.filter(cls.request_received_at.between(start_time, end_time))
|
|
353
|
+
.filter(cls.endpoint == endpoint)
|
|
354
|
+
.group_by("time_range")
|
|
355
|
+
.order_by("time_range")
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
results = session.execute(average_responses_lengths).fetchall()
|
|
359
|
+
return results
|
|
360
|
+
|
|
361
|
+
@classmethod
|
|
362
|
+
def get_responses_average_length_in_period(
|
|
363
|
+
cls,
|
|
364
|
+
start_time: datetime.datetime,
|
|
365
|
+
end_time: datetime.datetime,
|
|
366
|
+
time_format: str,
|
|
367
|
+
endpoint: Optional[EndpointType] = EndpointType.ANALYZE,
|
|
368
|
+
) -> dict[datetime.datetime, int]:
|
|
369
|
+
"""
|
|
370
|
+
Get a dictionary with average responses length
|
|
371
|
+
grouped by time units within a specified period.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
start_time (datetime): The start of the time period to query
|
|
375
|
+
end_time (datetime): The end of the time period to query
|
|
376
|
+
time_format (str): The strftime format string to format timestamps (e.g., '%Y-%m-%d')
|
|
377
|
+
endpoint (EndpointType): The analyze API endpoint to query
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
dict[datetime, int]: A dictionary mapping datetime objects
|
|
381
|
+
to average responses lengths
|
|
382
|
+
"""
|
|
383
|
+
with transaction(commit=False) as _:
|
|
384
|
+
average_responses_lengths = cls._get_average_responses_lengths_for_postgres(
|
|
385
|
+
start_time, end_time, time_format, endpoint
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
return cls.get_dictionary_with_datetime_keys(
|
|
389
|
+
time_format, average_responses_lengths
|
|
390
|
+
)
|
logdetective/server/metric.py
CHANGED
|
@@ -41,12 +41,10 @@ def update_metrics(
|
|
|
41
41
|
sent_at if sent_at else datetime.datetime.now(datetime.timezone.utc)
|
|
42
42
|
)
|
|
43
43
|
response_length = None
|
|
44
|
-
if hasattr(response, "explanation") and
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if "text" in choice
|
|
49
|
-
)
|
|
44
|
+
if hasattr(response, "explanation") and isinstance(
|
|
45
|
+
response.explanation, models.Explanation
|
|
46
|
+
):
|
|
47
|
+
response_length = len(response.explanation.text)
|
|
50
48
|
response_certainty = (
|
|
51
49
|
response.response_certainty if hasattr(response, "response_certainty") else None
|
|
52
50
|
)
|
logdetective/server/models.py
CHANGED
|
@@ -2,7 +2,9 @@ import datetime
|
|
|
2
2
|
from logging import BASIC_FORMAT
|
|
3
3
|
from typing import List, Dict, Optional, Literal
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel, Field, model_validator, field_validator
|
|
5
|
+
from pydantic import BaseModel, Field, model_validator, field_validator, NonNegativeFloat
|
|
6
|
+
|
|
7
|
+
from logdetective.constants import DEFAULT_TEMPERATURE
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class BuildLog(BaseModel):
|
|
@@ -95,6 +97,8 @@ class InferenceConfig(BaseModel):
|
|
|
95
97
|
)
|
|
96
98
|
url: str = ""
|
|
97
99
|
api_token: str = ""
|
|
100
|
+
model: str = ""
|
|
101
|
+
temperature: NonNegativeFloat = DEFAULT_TEMPERATURE
|
|
98
102
|
|
|
99
103
|
def __init__(self, data: Optional[dict] = None):
|
|
100
104
|
super().__init__()
|
|
@@ -106,6 +110,8 @@ class InferenceConfig(BaseModel):
|
|
|
106
110
|
self.api_endpoint = data.get("api_endpoint", "/chat/completions")
|
|
107
111
|
self.url = data.get("url", "")
|
|
108
112
|
self.api_token = data.get("api_token", "")
|
|
113
|
+
self.model = data.get("model", "default-model")
|
|
114
|
+
self.temperature = data.get("temperature", DEFAULT_TEMPERATURE)
|
|
109
115
|
|
|
110
116
|
|
|
111
117
|
class ExtractorConfig(BaseModel):
|
|
@@ -150,7 +156,8 @@ class LogConfig(BaseModel):
|
|
|
150
156
|
"""Logging configuration"""
|
|
151
157
|
|
|
152
158
|
name: str = "logdetective"
|
|
153
|
-
|
|
159
|
+
level_stream: str | int = "INFO"
|
|
160
|
+
level_file: str | int = "INFO"
|
|
154
161
|
path: str | None = None
|
|
155
162
|
format: str = BASIC_FORMAT
|
|
156
163
|
|
|
@@ -160,7 +167,8 @@ class LogConfig(BaseModel):
|
|
|
160
167
|
return
|
|
161
168
|
|
|
162
169
|
self.name = data.get("name", "logdetective")
|
|
163
|
-
self.
|
|
170
|
+
self.level_stream = data.get("level_stream", "INFO").upper()
|
|
171
|
+
self.level_file = data.get("level_file", "INFO").upper()
|
|
164
172
|
self.path = data.get("path")
|
|
165
173
|
self.format = data.get("format", BASIC_FORMAT)
|
|
166
174
|
|
logdetective/server/plot.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import datetime
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Optional, Union
|
|
3
3
|
|
|
4
4
|
import numpy
|
|
5
5
|
import matplotlib
|
|
@@ -62,24 +62,24 @@ class Definition:
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def create_time_series_arrays(
|
|
65
|
-
|
|
65
|
+
values_dict: dict[datetime.datetime, int],
|
|
66
|
+
plot_def: Definition,
|
|
66
67
|
start_time: datetime.datetime,
|
|
67
68
|
end_time: datetime.datetime,
|
|
68
|
-
|
|
69
|
-
time_format: str,
|
|
69
|
+
value_type: Optional[Union[int, float]] = int,
|
|
70
70
|
) -> tuple[numpy.ndarray, numpy.ndarray]:
|
|
71
|
-
"""Create time series arrays from a dictionary of
|
|
71
|
+
"""Create time series arrays from a dictionary of values.
|
|
72
72
|
|
|
73
73
|
This function generates two aligned numpy arrays:
|
|
74
74
|
1. An array of timestamps from start_time to end_time
|
|
75
|
-
2. A corresponding array of
|
|
75
|
+
2. A corresponding array of valuesfor each timestamp
|
|
76
76
|
|
|
77
77
|
The timestamps are truncated to the precision specified by time_format.
|
|
78
|
-
If a timestamp in
|
|
79
|
-
otherwise, the
|
|
78
|
+
If a timestamp in values_dict matches a generated timestamp, its values is used;
|
|
79
|
+
otherwise, the value defaults to zero.
|
|
80
80
|
|
|
81
81
|
Args:
|
|
82
|
-
|
|
82
|
+
values_dict: Dictionary mapping timestamps to their respective values
|
|
83
83
|
start_time: The starting timestamp of the time series
|
|
84
84
|
end_time: The ending timestamp of the time series
|
|
85
85
|
time_delta: The time interval between consecutive timestamps
|
|
@@ -88,67 +88,70 @@ def create_time_series_arrays(
|
|
|
88
88
|
Returns:
|
|
89
89
|
A tuple containing:
|
|
90
90
|
- numpy.ndarray: Array of timestamps
|
|
91
|
-
- numpy.ndarray: Array of corresponding
|
|
91
|
+
- numpy.ndarray: Array of corresponding values
|
|
92
92
|
"""
|
|
93
|
-
num_intervals = int((end_time - start_time) / time_delta) + 1
|
|
93
|
+
num_intervals = int((end_time - start_time) / plot_def.time_delta) + 1
|
|
94
94
|
|
|
95
95
|
timestamps = numpy.array(
|
|
96
96
|
[
|
|
97
97
|
datetime.datetime.strptime(
|
|
98
|
-
(start_time + i * time_delta).strftime(
|
|
98
|
+
(start_time + i * plot_def.time_delta).strftime(
|
|
99
|
+
format=plot_def.time_format
|
|
100
|
+
),
|
|
101
|
+
plot_def.time_format,
|
|
99
102
|
)
|
|
100
103
|
for i in range(num_intervals)
|
|
101
104
|
]
|
|
102
105
|
)
|
|
103
|
-
|
|
106
|
+
values = numpy.zeros(num_intervals, dtype=value_type)
|
|
104
107
|
|
|
105
108
|
timestamp_to_index = {timestamp: i for i, timestamp in enumerate(timestamps)}
|
|
106
109
|
|
|
107
|
-
for timestamp, count in
|
|
110
|
+
for timestamp, count in values_dict.items():
|
|
108
111
|
if timestamp in timestamp_to_index:
|
|
109
|
-
|
|
112
|
+
values[timestamp_to_index[timestamp]] = count
|
|
110
113
|
|
|
111
|
-
return timestamps,
|
|
114
|
+
return timestamps, values
|
|
112
115
|
|
|
113
116
|
|
|
114
|
-
def
|
|
115
|
-
|
|
117
|
+
def _add_bar_chart(
|
|
118
|
+
ax: matplotlib.figure.Axes,
|
|
116
119
|
plot_def: Definition,
|
|
117
120
|
timestamps: numpy.array,
|
|
118
|
-
|
|
121
|
+
values: numpy.array,
|
|
122
|
+
label: str,
|
|
119
123
|
) -> None:
|
|
120
|
-
"""Add a bar chart
|
|
124
|
+
"""Add a blue bar chart"""
|
|
121
125
|
bar_width = (
|
|
122
126
|
0.8 * plot_def.time_delta.total_seconds() / 86400
|
|
123
127
|
) # Convert to days for matplotlib
|
|
124
|
-
|
|
128
|
+
ax.bar(
|
|
125
129
|
timestamps,
|
|
126
|
-
|
|
130
|
+
values,
|
|
127
131
|
width=bar_width,
|
|
128
132
|
alpha=0.7,
|
|
129
133
|
color="skyblue",
|
|
130
|
-
label=
|
|
134
|
+
label=label,
|
|
131
135
|
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
ax.set_xlabel("Time")
|
|
137
|
+
ax.set_ylabel(label, color="blue")
|
|
138
|
+
ax.tick_params(axis="y", labelcolor="blue")
|
|
135
139
|
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter(plot_def.time_format))
|
|
141
|
+
ax.xaxis.set_major_locator(plot_def.locator)
|
|
138
142
|
|
|
139
143
|
matplotlib.pyplot.xticks(rotation=45)
|
|
140
144
|
|
|
141
|
-
|
|
145
|
+
ax.grid(True, alpha=0.3)
|
|
142
146
|
|
|
143
147
|
|
|
144
|
-
def
|
|
145
|
-
|
|
148
|
+
def _add_line_chart(
|
|
149
|
+
ax: matplotlib.figure.Axes, timestamps: numpy.array, values: numpy.array, label: str
|
|
146
150
|
) -> None:
|
|
147
|
-
"""Add
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
ax2.tick_params(axis="y", labelcolor="red")
|
|
151
|
+
"""Add a red line chart"""
|
|
152
|
+
ax.plot(timestamps, values, "r-", linewidth=2, label=label)
|
|
153
|
+
ax.set_ylabel(label, color="red")
|
|
154
|
+
ax.tick_params(axis="y", labelcolor="red")
|
|
152
155
|
|
|
153
156
|
|
|
154
157
|
def requests_per_time(
|
|
@@ -183,14 +186,14 @@ def requests_per_time(
|
|
|
183
186
|
start_time, end_time, plot_def.time_format, endpoint
|
|
184
187
|
)
|
|
185
188
|
timestamps, counts = create_time_series_arrays(
|
|
186
|
-
requests_counts, start_time, end_time
|
|
189
|
+
requests_counts, plot_def, start_time, end_time
|
|
187
190
|
)
|
|
188
191
|
|
|
189
192
|
fig, ax1 = matplotlib.pyplot.subplots(figsize=(12, 6))
|
|
190
|
-
|
|
193
|
+
_add_bar_chart(ax1, plot_def, timestamps, counts, "Requests")
|
|
191
194
|
|
|
192
195
|
ax2 = ax1.twinx()
|
|
193
|
-
|
|
196
|
+
_add_line_chart(ax2, timestamps, numpy.cumsum(counts), "Cumulative Requests")
|
|
194
197
|
|
|
195
198
|
matplotlib.pyplot.title(
|
|
196
199
|
f"Requests received for API {endpoint} ({start_time.strftime(plot_def.time_format)} "
|
|
@@ -204,3 +207,75 @@ def requests_per_time(
|
|
|
204
207
|
matplotlib.pyplot.tight_layout()
|
|
205
208
|
|
|
206
209
|
return fig
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def average_time_per_responses( # pylint: disable=too-many-locals
|
|
213
|
+
period_of_time: models.TimePeriod,
|
|
214
|
+
endpoint: EndpointType = EndpointType.ANALYZE,
|
|
215
|
+
end_time: Optional[datetime.datetime] = None,
|
|
216
|
+
) -> matplotlib.figure.Figure:
|
|
217
|
+
"""
|
|
218
|
+
Generate a visualization of average response time and length over a specified time period.
|
|
219
|
+
|
|
220
|
+
This function creates a dual-axis plot showing:
|
|
221
|
+
1. A bar chart of average response time per time interval
|
|
222
|
+
1. A line chart of average response length per time interval
|
|
223
|
+
|
|
224
|
+
The time intervals are determined by the provided TimePeriod object, which defines
|
|
225
|
+
the granularity and formatting of the time axis.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
period_of_time: A TimePeriod object that defines the time period and interval
|
|
229
|
+
for the analysis (e.g., hourly, daily, weekly)
|
|
230
|
+
endpoint: One of the API endpoints
|
|
231
|
+
end_time: The end time for the analysis period. If None, defaults to the current
|
|
232
|
+
UTC time
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
A matplotlib Figure object containing the generated visualization
|
|
236
|
+
"""
|
|
237
|
+
end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
|
|
238
|
+
start_time = period_of_time.get_period_start_time(end_time)
|
|
239
|
+
plot_def = Definition(period_of_time)
|
|
240
|
+
responses_average_time = AnalyzeRequestMetrics.get_responses_average_time_in_period(
|
|
241
|
+
start_time, end_time, plot_def.time_format, endpoint
|
|
242
|
+
)
|
|
243
|
+
timestamps, average_time = create_time_series_arrays(
|
|
244
|
+
responses_average_time,
|
|
245
|
+
plot_def,
|
|
246
|
+
start_time,
|
|
247
|
+
end_time,
|
|
248
|
+
float,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
fig, ax1 = matplotlib.pyplot.subplots(figsize=(12, 6))
|
|
252
|
+
_add_bar_chart(ax1, plot_def, timestamps, average_time, "average response time (seconds)")
|
|
253
|
+
|
|
254
|
+
responses_average_length = (
|
|
255
|
+
AnalyzeRequestMetrics.get_responses_average_length_in_period(
|
|
256
|
+
start_time, end_time, plot_def.time_format, endpoint
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
timestamps, average_length = create_time_series_arrays(
|
|
260
|
+
responses_average_length,
|
|
261
|
+
plot_def,
|
|
262
|
+
start_time,
|
|
263
|
+
end_time,
|
|
264
|
+
float,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
ax2 = ax1.twinx()
|
|
268
|
+
_add_line_chart(ax2, timestamps, average_length, "average response length (chars)")
|
|
269
|
+
|
|
270
|
+
matplotlib.pyplot.title(
|
|
271
|
+
f"average response time for API {endpoint} ({start_time.strftime(plot_def.time_format)} "
|
|
272
|
+
f"to {end_time.strftime(plot_def.time_format)})"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
lines1, labels1 = ax1.get_legend_handles_labels()
|
|
276
|
+
lines2, labels2 = ax2.get_legend_handles_labels()
|
|
277
|
+
ax1.legend(lines1 + lines2, labels1 + labels2, loc="center")
|
|
278
|
+
|
|
279
|
+
matplotlib.pyplot.tight_layout()
|
|
280
|
+
|
|
281
|
+
return fig
|
logdetective/server/server.py
CHANGED
|
@@ -186,7 +186,6 @@ async def submit_text( # pylint: disable=R0913,R0917
|
|
|
186
186
|
log_probs: int = 1,
|
|
187
187
|
stream: bool = False,
|
|
188
188
|
model: str = "default-model",
|
|
189
|
-
api_endpoint: str = "/chat/completions",
|
|
190
189
|
) -> Explanation:
|
|
191
190
|
"""Submit prompt to LLM using a selected endpoint.
|
|
192
191
|
max_tokens: number of tokens to be produces, 0 indicates run until encountering EOS
|
|
@@ -199,7 +198,7 @@ async def submit_text( # pylint: disable=R0913,R0917
|
|
|
199
198
|
if SERVER_CONFIG.inference.api_token:
|
|
200
199
|
headers["Authorization"] = f"Bearer {SERVER_CONFIG.inference.api_token}"
|
|
201
200
|
|
|
202
|
-
if api_endpoint == "/chat/completions":
|
|
201
|
+
if SERVER_CONFIG.inference.api_endpoint == "/chat/completions":
|
|
203
202
|
return await submit_text_chat_completions(
|
|
204
203
|
text, headers, max_tokens, log_probs > 0, stream, model
|
|
205
204
|
)
|
|
@@ -227,6 +226,7 @@ async def submit_text_completions( # pylint: disable=R0913,R0917
|
|
|
227
226
|
"logprobs": log_probs,
|
|
228
227
|
"stream": stream,
|
|
229
228
|
"model": model,
|
|
229
|
+
"temperature": SERVER_CONFIG.inference.temperature,
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
response = await submit_to_llm_endpoint(
|
|
@@ -266,6 +266,7 @@ async def submit_text_chat_completions( # pylint: disable=R0913,R0917
|
|
|
266
266
|
"logprobs": log_probs,
|
|
267
267
|
"stream": stream,
|
|
268
268
|
"model": model,
|
|
269
|
+
"temperature": SERVER_CONFIG.inference.temperature,
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
response = await submit_to_llm_endpoint(
|
|
@@ -300,7 +301,8 @@ async def analyze_log(build_log: BuildLog):
|
|
|
300
301
|
log_summary = format_snippets(log_summary)
|
|
301
302
|
response = await submit_text(
|
|
302
303
|
PROMPT_CONFIG.prompt_template.format(log_summary),
|
|
303
|
-
|
|
304
|
+
model=SERVER_CONFIG.inference.model,
|
|
305
|
+
max_tokens=SERVER_CONFIG.inference.max_tokens,
|
|
304
306
|
)
|
|
305
307
|
certainty = 0
|
|
306
308
|
|
|
@@ -340,7 +342,8 @@ async def perform_staged_analysis(log_text: str) -> StagedResponse:
|
|
|
340
342
|
*[
|
|
341
343
|
submit_text(
|
|
342
344
|
PROMPT_CONFIG.snippet_prompt_template.format(s),
|
|
343
|
-
|
|
345
|
+
model=SERVER_CONFIG.inference.model,
|
|
346
|
+
max_tokens=SERVER_CONFIG.inference.max_tokens,
|
|
344
347
|
)
|
|
345
348
|
for s in log_summary
|
|
346
349
|
]
|
|
@@ -355,7 +358,9 @@ async def perform_staged_analysis(log_text: str) -> StagedResponse:
|
|
|
355
358
|
)
|
|
356
359
|
|
|
357
360
|
final_analysis = await submit_text(
|
|
358
|
-
final_prompt,
|
|
361
|
+
final_prompt,
|
|
362
|
+
model=SERVER_CONFIG.inference.model,
|
|
363
|
+
max_tokens=SERVER_CONFIG.inference.max_tokens,
|
|
359
364
|
)
|
|
360
365
|
|
|
361
366
|
certainty = 0
|
|
@@ -396,7 +401,9 @@ async def analyze_log_stream(build_log: BuildLog):
|
|
|
396
401
|
headers["Authorization"] = f"Bearer {SERVER_CONFIG.inference.api_token}"
|
|
397
402
|
|
|
398
403
|
stream = await submit_text_chat_completions(
|
|
399
|
-
PROMPT_CONFIG.prompt_template.format(log_summary), stream=True, headers=headers
|
|
404
|
+
PROMPT_CONFIG.prompt_template.format(log_summary), stream=True, headers=headers,
|
|
405
|
+
model=SERVER_CONFIG.inference.model,
|
|
406
|
+
max_tokens=SERVER_CONFIG.inference.max_tokens,
|
|
400
407
|
)
|
|
401
408
|
|
|
402
409
|
return StreamingResponse(stream)
|
|
@@ -616,8 +623,8 @@ async def comment_on_mr(
|
|
|
616
623
|
response.explanation.text,
|
|
617
624
|
)
|
|
618
625
|
|
|
619
|
-
# Get the formatted comment.
|
|
620
|
-
|
|
626
|
+
# Get the formatted short comment.
|
|
627
|
+
short_comment = await generate_mr_comment(job, log_url, response, full=False)
|
|
621
628
|
|
|
622
629
|
# Look up the merge request
|
|
623
630
|
merge_request = await asyncio.to_thread(
|
|
@@ -625,11 +632,33 @@ async def comment_on_mr(
|
|
|
625
632
|
)
|
|
626
633
|
|
|
627
634
|
# Submit a new comment to the Merge Request using the Gitlab API
|
|
628
|
-
await asyncio.to_thread(
|
|
635
|
+
discussion = await asyncio.to_thread(
|
|
636
|
+
merge_request.discussions.create, {"body": short_comment}
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
# Get the ID of the first note
|
|
640
|
+
note_id = discussion.attributes["notes"][0]["id"]
|
|
641
|
+
note = discussion.notes.get(note_id)
|
|
642
|
+
|
|
643
|
+
# Update the comment with the full details
|
|
644
|
+
# We do this in a second step so we don't bombard the user's email
|
|
645
|
+
# notifications with a massive message. Gitlab doesn't send email for
|
|
646
|
+
# comment edits.
|
|
647
|
+
full_comment = await generate_mr_comment(job, log_url, response, full=True)
|
|
648
|
+
note.body = full_comment
|
|
649
|
+
|
|
650
|
+
# Pause for five seconds before sending the snippet data, otherwise
|
|
651
|
+
# Gitlab may bundle the edited message together with the creation
|
|
652
|
+
# message in email.
|
|
653
|
+
await asyncio.sleep(5)
|
|
654
|
+
await asyncio.to_thread(note.save)
|
|
629
655
|
|
|
630
656
|
|
|
631
657
|
async def generate_mr_comment(
|
|
632
|
-
job: gitlab.v4.objects.ProjectJob,
|
|
658
|
+
job: gitlab.v4.objects.ProjectJob,
|
|
659
|
+
log_url: str,
|
|
660
|
+
response: StagedResponse,
|
|
661
|
+
full: bool = True,
|
|
633
662
|
) -> str:
|
|
634
663
|
"""Use a template to generate a comment string to submit to Gitlab"""
|
|
635
664
|
|
|
@@ -637,7 +666,11 @@ async def generate_mr_comment(
|
|
|
637
666
|
script_path = Path(__file__).resolve().parent
|
|
638
667
|
template_path = Path(script_path, "templates")
|
|
639
668
|
jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_path))
|
|
640
|
-
|
|
669
|
+
|
|
670
|
+
if full:
|
|
671
|
+
tpl = jinja_env.get_template("gitlab_full_comment.md.j2")
|
|
672
|
+
else:
|
|
673
|
+
tpl = jinja_env.get_template("gitlab_short_comment.md.j2")
|
|
641
674
|
|
|
642
675
|
artifacts_url = f"{job.project_url}/-/jobs/{job.id}/artifacts/download"
|
|
643
676
|
|
|
@@ -676,6 +709,35 @@ def _svg_figure_response(fig: matplotlib.figure.Figure):
|
|
|
676
709
|
)
|
|
677
710
|
|
|
678
711
|
|
|
712
|
+
def _multiple_svg_figures_response(figures: list[matplotlib.figure.Figure]):
|
|
713
|
+
"""Create a response with multiple svg figures."""
|
|
714
|
+
svg_contents = []
|
|
715
|
+
for i, fig in enumerate(figures):
|
|
716
|
+
buf = BytesIO()
|
|
717
|
+
fig.savefig(buf, format="svg", bbox_inches="tight")
|
|
718
|
+
matplotlib.pyplot.close(fig)
|
|
719
|
+
buf.seek(0)
|
|
720
|
+
svg_contents.append(buf.read().decode("utf-8"))
|
|
721
|
+
|
|
722
|
+
html_content = "<html><body>\n"
|
|
723
|
+
for i, svg in enumerate(svg_contents):
|
|
724
|
+
html_content += f"<div id='figure-{i}'>\n{svg}\n</div>\n"
|
|
725
|
+
html_content += "</body></html>"
|
|
726
|
+
|
|
727
|
+
return BasicResponse(content=html_content, media_type="text/html")
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
@app.get("/metrics/analyze", response_class=StreamingResponse)
|
|
731
|
+
async def show_analyze_metrics(period_since_now: TimePeriod = Depends(TimePeriod)):
|
|
732
|
+
"""Show statistics for requests and responses in the given period of time
|
|
733
|
+
for the /analyze API endpoint."""
|
|
734
|
+
fig_requests = plot.requests_per_time(period_since_now, EndpointType.ANALYZE)
|
|
735
|
+
fig_responses = plot.average_time_per_responses(
|
|
736
|
+
period_since_now, EndpointType.ANALYZE
|
|
737
|
+
)
|
|
738
|
+
return _multiple_svg_figures_response([fig_requests, fig_responses])
|
|
739
|
+
|
|
740
|
+
|
|
679
741
|
@app.get("/metrics/analyze/requests", response_class=StreamingResponse)
|
|
680
742
|
async def show_analyze_requests(period_since_now: TimePeriod = Depends(TimePeriod)):
|
|
681
743
|
"""Show statistics for the requests received in the given period of time
|
|
@@ -684,6 +746,27 @@ async def show_analyze_requests(period_since_now: TimePeriod = Depends(TimePerio
|
|
|
684
746
|
return _svg_figure_response(fig)
|
|
685
747
|
|
|
686
748
|
|
|
749
|
+
@app.get("/metrics/analyze/responses", response_class=StreamingResponse)
|
|
750
|
+
async def show_analyze_responses(period_since_now: TimePeriod = Depends(TimePeriod)):
|
|
751
|
+
"""Show statistics for responses given in the specified period of time
|
|
752
|
+
for the /analyze API endpoint."""
|
|
753
|
+
fig = plot.average_time_per_responses(period_since_now, EndpointType.ANALYZE)
|
|
754
|
+
return _svg_figure_response(fig)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
@app.get("/metrics/analyze/staged", response_class=StreamingResponse)
|
|
758
|
+
async def show_analyze_staged_metrics(
|
|
759
|
+
period_since_now: TimePeriod = Depends(TimePeriod),
|
|
760
|
+
):
|
|
761
|
+
"""Show statistics for requests and responses in the given period of time
|
|
762
|
+
for the /analyze/staged API endpoint."""
|
|
763
|
+
fig_requests = plot.requests_per_time(period_since_now, EndpointType.ANALYZE_STAGED)
|
|
764
|
+
fig_responses = plot.average_time_per_responses(
|
|
765
|
+
period_since_now, EndpointType.ANALYZE_STAGED
|
|
766
|
+
)
|
|
767
|
+
return _multiple_svg_figures_response([fig_requests, fig_responses])
|
|
768
|
+
|
|
769
|
+
|
|
687
770
|
@app.get("/metrics/analyze/staged/requests", response_class=StreamingResponse)
|
|
688
771
|
async def show_analyze_staged_requests(
|
|
689
772
|
period_since_now: TimePeriod = Depends(TimePeriod),
|
|
@@ -692,3 +775,13 @@ async def show_analyze_staged_requests(
|
|
|
692
775
|
for the /analyze/staged API endpoint."""
|
|
693
776
|
fig = plot.requests_per_time(period_since_now, EndpointType.ANALYZE_STAGED)
|
|
694
777
|
return _svg_figure_response(fig)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
@app.get("/metrics/analyze/staged/responses", response_class=StreamingResponse)
|
|
781
|
+
async def show_analyze_staged_responses(
|
|
782
|
+
period_since_now: TimePeriod = Depends(TimePeriod),
|
|
783
|
+
):
|
|
784
|
+
"""Show statistics for responses given in the specified period of time
|
|
785
|
+
for the /analyze/staged API endpoint."""
|
|
786
|
+
fig = plot.average_time_per_responses(period_since_now, EndpointType.ANALYZE_STAGED)
|
|
787
|
+
return _svg_figure_response(fig)
|
|
@@ -9,9 +9,7 @@ In this case, we are {{ certainty }}% certain of the response {{ emoji_face }}.
|
|
|
9
9
|
<ul>
|
|
10
10
|
{% for snippet in snippets %}
|
|
11
11
|
<li>
|
|
12
|
-
<code>
|
|
13
|
-
Line {{ snippet.line_number }}: {{ snippet.text }}
|
|
14
|
-
</code>
|
|
12
|
+
<b>Line {{ snippet.line_number }}:</b> <code>{{ snippet.text }}</code>
|
|
15
13
|
{{ snippet.explanation }}
|
|
16
14
|
</li>
|
|
17
15
|
{% endfor %}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
The package {{ package }} failed to build, here is a possible explanation why.
|
|
2
|
+
|
|
3
|
+
Please know that the explanation was provided by AI and may be incorrect.
|
|
4
|
+
In this case, we are {{ certainty }}% certain of the response {{ emoji_face }}.
|
|
5
|
+
|
|
6
|
+
{{ explanation }}
|
|
7
|
+
|
|
8
|
+
<details>
|
|
9
|
+
<summary>Logs</summary>
|
|
10
|
+
<p>
|
|
11
|
+
Log Detective analyzed the following logs files to provide an explanation:
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<ul>
|
|
15
|
+
<li><a href="{{ log_url }}">{{ log_url }}</a></li>
|
|
16
|
+
</ul>
|
|
17
|
+
|
|
18
|
+
<p>
|
|
19
|
+
Additional logs are available from:
|
|
20
|
+
<ul>
|
|
21
|
+
<li><a href="{{ artifacts_url }}">artifacts.zip</a></li>
|
|
22
|
+
</ul>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p>
|
|
26
|
+
Please know that these log files are automatically removed after some
|
|
27
|
+
time, so you might need a backup.
|
|
28
|
+
</p>
|
|
29
|
+
</details>
|
|
30
|
+
|
|
31
|
+
<details>
|
|
32
|
+
<summary>Help</summary>
|
|
33
|
+
<p>Don't hesitate to reach out.</p>
|
|
34
|
+
|
|
35
|
+
<ul>
|
|
36
|
+
<li><a href="https://github.com/fedora-copr/logdetective">Upstream</a></li>
|
|
37
|
+
<li><a href="https://github.com/fedora-copr/logdetective/issues">Issue tracker</a></li>
|
|
38
|
+
<li><a href="https://redhat.enterprise.slack.com/archives/C06DWNVKKDE">Slack</a></li>
|
|
39
|
+
<li><a href="https://log-detective.com/documentation">Documentation</a></li>
|
|
40
|
+
</ul>
|
|
41
|
+
</details>
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
This comment was created by [Log Detective][log-detective].
|
|
46
|
+
|
|
47
|
+
Was the provided feedback accurate and helpful? <br>Please vote with :thumbsup:
|
|
48
|
+
or :thumbsdown: to help us improve.<br>
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
[log-detective]: https://log-detective.com/
|
|
53
|
+
[contact]: https://github.com/fedora-copr
|
logdetective/server/utils.py
CHANGED
|
@@ -37,7 +37,7 @@ def get_log(config: Config):
|
|
|
37
37
|
if getattr(log, "initialized", False):
|
|
38
38
|
return log
|
|
39
39
|
|
|
40
|
-
log.setLevel(
|
|
40
|
+
log.setLevel("DEBUG")
|
|
41
41
|
|
|
42
42
|
# Drop the default handler, we will create it ourselves
|
|
43
43
|
log.handlers = []
|
|
@@ -45,12 +45,14 @@ def get_log(config: Config):
|
|
|
45
45
|
# STDOUT
|
|
46
46
|
stream_handler = logging.StreamHandler()
|
|
47
47
|
stream_handler.setFormatter(logging.Formatter(config.log.format))
|
|
48
|
+
stream_handler.setLevel(config.log.level_stream)
|
|
48
49
|
log.addHandler(stream_handler)
|
|
49
50
|
|
|
50
51
|
# Log to file
|
|
51
52
|
if config.log.path:
|
|
52
53
|
file_handler = logging.FileHandler(config.log.path)
|
|
53
54
|
file_handler.setFormatter(logging.Formatter(config.log.format))
|
|
55
|
+
file_handler.setLevel(config.log.level_file)
|
|
54
56
|
log.addHandler(file_handler)
|
|
55
57
|
|
|
56
58
|
log.initialized = True
|
logdetective/utils.py
CHANGED
|
@@ -111,19 +111,23 @@ def compute_certainty(probs: List[Dict]) -> float:
|
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
def process_log(
|
|
114
|
-
log: str, model: Llama, stream: bool, prompt_template: str
|
|
114
|
+
log: str, model: Llama, stream: bool, prompt_template: str,
|
|
115
|
+
temperature: float
|
|
115
116
|
) -> CreateCompletionResponse | Iterator[CreateCompletionStreamResponse]:
|
|
116
117
|
"""Processes a given log using the provided language model and returns its summary.
|
|
117
118
|
|
|
118
119
|
Args:
|
|
119
120
|
log (str): The input log to be processed.
|
|
120
121
|
model (Llama): The language model used for processing the log.
|
|
121
|
-
|
|
122
|
+
stream (bool): Return output as Iterator.
|
|
123
|
+
prompt_template (str): Which prompt template to use.
|
|
124
|
+
temperature (float): Temperature parameter for model runtime.
|
|
122
125
|
Returns:
|
|
123
126
|
str: The summary of the given log generated by the language model.
|
|
124
127
|
"""
|
|
125
128
|
response = model(
|
|
126
|
-
prompt=prompt_template.format(log), stream=stream, max_tokens=0, logprobs=1
|
|
129
|
+
prompt=prompt_template.format(log), stream=stream, max_tokens=0, logprobs=1,
|
|
130
|
+
temperature=temperature
|
|
127
131
|
)
|
|
128
132
|
|
|
129
133
|
return response
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: logdetective
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.11
|
|
4
4
|
Summary: Log using LLM AI to search for build/test failures and provide ideas for fixing these.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Author: Jiri Podivin
|
|
@@ -47,6 +47,8 @@ Log Detective
|
|
|
47
47
|
|
|
48
48
|
A Python tool to analyze logs using a Language Model (LLM) and Drain template miner.
|
|
49
49
|
|
|
50
|
+
Note: if you are looking for code of website logdetective.com it is in [github.com/fedora-copr/logdetective-website](https://github.com/fedora-copr/logdetective-website).
|
|
51
|
+
|
|
50
52
|
Installation
|
|
51
53
|
------------
|
|
52
54
|
|
|
@@ -95,6 +97,17 @@ Example you want to use a different model:
|
|
|
95
97
|
logdetective https://example.com/logs.txt --model https://huggingface.co/QuantFactory/Meta-Llama-3-8B-Instruct-GGUF/resolve/main/Meta-Llama-3-8B-Instruct.Q5_K_S.gguf?download=true
|
|
96
98
|
logdetective https://example.com/logs.txt --model QuantFactory/Meta-Llama-3-8B-Instruct-GGUF
|
|
97
99
|
|
|
100
|
+
Example of different suffix (useful for models that were quantized)
|
|
101
|
+
|
|
102
|
+
logdetective https://kojipkgs.fedoraproject.org//work/tasks/3367/131313367/build.log --model 'fedora-copr/granite-3.2-8b-instruct-GGUF' -F Q4_K.gguf
|
|
103
|
+
|
|
104
|
+
Example of altered prompts:
|
|
105
|
+
|
|
106
|
+
cp ~/.local/lib/python3.13/site-packages/logdetective/prompts.yml ~/my-prompts.yml
|
|
107
|
+
vi ~/my-prompts.yml # edit the prompts there to better fit your needs
|
|
108
|
+
logdetective https://kojipkgs.fedoraproject.org//work/tasks/3367/131313367/build.log --prompts ~/my-prompts.yml
|
|
109
|
+
|
|
110
|
+
|
|
98
111
|
Note that streaming with some models (notably Meta-Llama-3 is broken) is broken and can be workarounded by `no-stream` option:
|
|
99
112
|
|
|
100
113
|
logdetective https://example.com/logs.txt --model QuantFactory/Meta-Llama-3-8B-Instruct-GGUF --no-stream
|
|
@@ -337,11 +350,23 @@ certbot certonly --standalone -d logdetective01.fedorainfracloud.org
|
|
|
337
350
|
Querying statistics
|
|
338
351
|
-------------------
|
|
339
352
|
|
|
340
|
-
You can retrieve statistics about server requests over a specified time period
|
|
341
|
-
using either the `curl`
|
|
353
|
+
You can retrieve statistics about server requests and responses over a specified time period
|
|
354
|
+
using either a browser, the `curl` or the `http` command (provided by the `httpie` package).
|
|
342
355
|
|
|
343
356
|
When no time period is specified, the query defaults to the last 2 days:
|
|
344
357
|
|
|
358
|
+
You can view requests and responses statistics
|
|
359
|
+
- for the `/analyze` endpoint at http://localhost:8080/metrics/analyze
|
|
360
|
+
- for the `/analyze/staged` endpoint at http://localhost:8080/metrics/analyze/staged.
|
|
361
|
+
|
|
362
|
+
You can retrieve single svg images at the following endpoints:
|
|
363
|
+
- `/metrics/analyze/requests`
|
|
364
|
+
- `/metrics/analyze/responses`
|
|
365
|
+
- `/metrics/analyze/staged/requests`
|
|
366
|
+
- `/metrics/analyze/stages/responses`
|
|
367
|
+
|
|
368
|
+
Examples:
|
|
369
|
+
|
|
345
370
|
```
|
|
346
371
|
http GET "localhost:8080/metrics/analyze/requests" > /tmp/plot.svg
|
|
347
372
|
curl "localhost:8080/metrics/analyze/staged/requests" > /tmp/plot.svg
|
|
@@ -349,7 +374,6 @@ curl "localhost:8080/metrics/analyze/staged/requests" > /tmp/plot.svg
|
|
|
349
374
|
|
|
350
375
|
You can specify the time period in hours, days, or weeks.
|
|
351
376
|
The time period:
|
|
352
|
-
|
|
353
377
|
- cannot be less than one hour
|
|
354
378
|
- cannot be negative
|
|
355
379
|
- ends at the current time (when the query is made)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
logdetective/__init__.py,sha256=VqRngDcuFT7JWms8Qc_MsOvajoXVOKPr-S1kqY3Pqhc,59
|
|
2
|
+
logdetective/constants.py,sha256=A5PzeqlQqDbBS_kzP2hl-lhJ0lCEqdbvW3CaQUYVxjw,1849
|
|
3
|
+
logdetective/drain3.ini,sha256=ni91eCT1TwTznZwcqWoOVMQcGEnWhEDNCoTPF7cfGfY,1360
|
|
4
|
+
logdetective/extractors.py,sha256=7ahzWbTtU9MveG1Q7wU9LO8OJgs85X-cHmWltUhCe9M,3491
|
|
5
|
+
logdetective/logdetective.py,sha256=Q1SfQ9sWR5sIvHJag61-F-8edwf7p1SV7QZRg9VaWcc,5604
|
|
6
|
+
logdetective/models.py,sha256=nrGBmMRu8i6UhFflQKAp81Y3Sd_Aaoor0i_yqSJoLT0,1115
|
|
7
|
+
logdetective/prompts.yml,sha256=dMW2-bdTIqv7LF_owqRD4xinMK5ZWcNhDynnX1zoKns,1722
|
|
8
|
+
logdetective/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
logdetective/server/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
logdetective/server/database/base.py,sha256=oMJUvbWeapIUP-8Cf_DR9ptFg8CsYeaBAIjOVEzx8SM,1668
|
|
11
|
+
logdetective/server/database/models.py,sha256=m_3qNBWJwLSwjJn0AmwSxXMJk75Gu1bXFtGAP_4zps4,14088
|
|
12
|
+
logdetective/server/metric.py,sha256=-uM_-yqxNA-EZTCnNRdQ8g1MicmE5eC6jRFI_mBBYUg,2606
|
|
13
|
+
logdetective/server/models.py,sha256=URqZcfx5yUsifZ1pOwZ_uU3Tyjcdvuq6qEnAvTexl4A,8475
|
|
14
|
+
logdetective/server/plot.py,sha256=B2rOngqx7g-Z3NfttboTip3frkypdF1H7FhK8vh45mE,9655
|
|
15
|
+
logdetective/server/server.py,sha256=4NylBojHm9E3gjByVWs870T204ls39EbZmUfU0Kyq4U,28395
|
|
16
|
+
logdetective/server/templates/gitlab_full_comment.md.j2,sha256=DQZ2WVFedpuXI6znbHIW4wpF9BmFS8FaUkowh8AnGhE,1627
|
|
17
|
+
logdetective/server/templates/gitlab_short_comment.md.j2,sha256=fzScpayv2vpRLczP_0O0YxtA8rsKvR6gSv4ntNdWb98,1443
|
|
18
|
+
logdetective/server/utils.py,sha256=QO0H1q55YLCLKxkViqex4Uu31LnakpYUKJfZHysonSc,1838
|
|
19
|
+
logdetective/utils.py,sha256=nklnTipAet9P9aEiuHcnK62WT0DmNHbvO1TvNlrxlik,6463
|
|
20
|
+
logdetective-0.5.11.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
21
|
+
logdetective-0.5.11.dist-info/METADATA,sha256=LOOzu99kJaP02U2OaFQciPdWKhlgr4Vm4tVKijTY7NM,15882
|
|
22
|
+
logdetective-0.5.11.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
23
|
+
logdetective-0.5.11.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
|
|
24
|
+
logdetective-0.5.11.dist-info/RECORD,,
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
logdetective/__init__.py,sha256=VqRngDcuFT7JWms8Qc_MsOvajoXVOKPr-S1kqY3Pqhc,59
|
|
2
|
-
logdetective/constants.py,sha256=eiS6eYhEgl_Rlyi_B9j00DDp9A-UDhuFz3ACWtKf_SU,1558
|
|
3
|
-
logdetective/drain3.ini,sha256=ni91eCT1TwTznZwcqWoOVMQcGEnWhEDNCoTPF7cfGfY,1360
|
|
4
|
-
logdetective/extractors.py,sha256=7ahzWbTtU9MveG1Q7wU9LO8OJgs85X-cHmWltUhCe9M,3491
|
|
5
|
-
logdetective/logdetective.py,sha256=SDuzeS9sMp7rs6cTZAEd0ajtyWv9XnDkEPTF82nwaYo,5390
|
|
6
|
-
logdetective/models.py,sha256=nrGBmMRu8i6UhFflQKAp81Y3Sd_Aaoor0i_yqSJoLT0,1115
|
|
7
|
-
logdetective/prompts.yml,sha256=3orDNqqZNadWCaNncgfk8D3Pqqef4IzfScoa_jUJzCY,1452
|
|
8
|
-
logdetective/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
logdetective/server/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
logdetective/server/database/base.py,sha256=oMJUvbWeapIUP-8Cf_DR9ptFg8CsYeaBAIjOVEzx8SM,1668
|
|
11
|
-
logdetective/server/database/models.py,sha256=arIahOCT-hTmh904DXrWSkH7rlo13Ppu-OO80huX5Dc,6118
|
|
12
|
-
logdetective/server/metric.py,sha256=VYMifrfIhcqgyu6YYN0c1nt8fC1iJ2_LCB7Bh2AheoE,2679
|
|
13
|
-
logdetective/server/models.py,sha256=cf1ngu_-19rP_i49s5cEwIzh6SfL_ZpVy4EykCpfWck,8076
|
|
14
|
-
logdetective/server/plot.py,sha256=3o-CNHjel04ekpwSB4ckV7dbiF663cfPkimQ0aP9U_8,7073
|
|
15
|
-
logdetective/server/server.py,sha256=FDKx-6wsVoEwdEgcoepAT3GL0gZKjMSpB1VU-jaKt2w,24618
|
|
16
|
-
logdetective/server/templates/gitlab_comment.md.j2,sha256=kheTkhQ-LfuFkr8av-Mw2a-9VYEUbDTLwaa-CKI6OkI,1622
|
|
17
|
-
logdetective/server/utils.py,sha256=6y4gZCwQG4HcjWJwYdzwP46Jsm3xoNXZWH4kYmSWVZA,1741
|
|
18
|
-
logdetective/utils.py,sha256=_cBBkBwZHX5qxy0K5WK2MnHA4x_oor7R-QED2VZLbCA,6226
|
|
19
|
-
logdetective-0.5.10.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
20
|
-
logdetective-0.5.10.dist-info/METADATA,sha256=NbD3YEoEU-YAhH-VjOo95qWxyk1T1bq5wCih4N5oyqs,14738
|
|
21
|
-
logdetective-0.5.10.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
22
|
-
logdetective-0.5.10.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
|
|
23
|
-
logdetective-0.5.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|