logdetective 0.10.0__tar.gz → 0.11.1__tar.gz
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-0.10.0 → logdetective-0.11.1}/PKG-INFO +3 -2
- {logdetective-0.10.0 → logdetective-0.11.1}/README.md +2 -1
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/database/models/merge_request_jobs.py +17 -1
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/llm.py +1 -1
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/metric.py +18 -1
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/plot.py +157 -9
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/server.py +11 -2
- {logdetective-0.10.0 → logdetective-0.11.1}/pyproject.toml +1 -1
- {logdetective-0.10.0 → logdetective-0.11.1}/LICENSE +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/__init__.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/constants.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/drain3.ini +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/extractors.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/logdetective.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/models.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/prompts.yml +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/remote_log.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/__init__.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/compressors.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/config.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/database/__init__.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/database/base.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/database/models/__init__.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/database/models/metrics.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/emoji.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/gitlab.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/models.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/templates/gitlab_full_comment.md.j2 +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/templates/gitlab_short_comment.md.j2 +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective/utils.py +0 -0
- {logdetective-0.10.0 → logdetective-0.11.1}/logdetective.1.asciidoc +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: logdetective
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.1
|
|
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
|
|
@@ -358,7 +358,7 @@ using either a browser, the `curl` or the `http` command (provided by the `httpi
|
|
|
358
358
|
|
|
359
359
|
When no time period is specified, the query defaults to the last 2 days:
|
|
360
360
|
|
|
361
|
-
You can view requests and
|
|
361
|
+
You can view requests, responses and emojis statistics
|
|
362
362
|
- for the `/analyze` endpoint at http://localhost:8080/metrics/analyze
|
|
363
363
|
- for the `/analyze-staged` endpoint at http://localhost:8080/metrics/analyze-staged.
|
|
364
364
|
- for the requests coming from gitlab: http://localhost:8080/metrics/analyze-gitlab.
|
|
@@ -370,6 +370,7 @@ You can retrieve single svg images at the following endpoints:
|
|
|
370
370
|
- http://localhost:8080/metrics/analyze-staged/responses
|
|
371
371
|
- http://localhost:8080/metrics/analyze-gitlab/requests
|
|
372
372
|
- http://localhost:8080/metrics/analyze-gitlab/responses
|
|
373
|
+
- http://localhost:8080/metrics/analyze-gitlab/emojis
|
|
373
374
|
|
|
374
375
|
Examples:
|
|
375
376
|
|
|
@@ -315,7 +315,7 @@ using either a browser, the `curl` or the `http` command (provided by the `httpi
|
|
|
315
315
|
|
|
316
316
|
When no time period is specified, the query defaults to the last 2 days:
|
|
317
317
|
|
|
318
|
-
You can view requests and
|
|
318
|
+
You can view requests, responses and emojis statistics
|
|
319
319
|
- for the `/analyze` endpoint at http://localhost:8080/metrics/analyze
|
|
320
320
|
- for the `/analyze-staged` endpoint at http://localhost:8080/metrics/analyze-staged.
|
|
321
321
|
- for the requests coming from gitlab: http://localhost:8080/metrics/analyze-gitlab.
|
|
@@ -327,6 +327,7 @@ You can retrieve single svg images at the following endpoints:
|
|
|
327
327
|
- http://localhost:8080/metrics/analyze-staged/responses
|
|
328
328
|
- http://localhost:8080/metrics/analyze-gitlab/requests
|
|
329
329
|
- http://localhost:8080/metrics/analyze-gitlab/responses
|
|
330
|
+
- http://localhost:8080/metrics/analyze-gitlab/emojis
|
|
330
331
|
|
|
331
332
|
Examples:
|
|
332
333
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import enum
|
|
2
2
|
import datetime
|
|
3
|
-
from typing import Optional, List
|
|
3
|
+
from typing import Optional, List, Tuple
|
|
4
4
|
|
|
5
5
|
import backoff
|
|
6
6
|
|
|
@@ -585,3 +585,19 @@ class Reactions(Base):
|
|
|
585
585
|
with transaction(commit=True) as session:
|
|
586
586
|
session.delete(reaction)
|
|
587
587
|
session.flush()
|
|
588
|
+
|
|
589
|
+
@classmethod
|
|
590
|
+
def get_since(
|
|
591
|
+
cls, time: datetime.datetime
|
|
592
|
+
) -> List[Tuple[datetime.datetime, "Comments"]]:
|
|
593
|
+
"""Get all the reactions on comments created after the given time
|
|
594
|
+
and the comment creation time."""
|
|
595
|
+
with transaction(commit=False) as session:
|
|
596
|
+
reactions = (
|
|
597
|
+
session.query(Comments.created_at, cls)
|
|
598
|
+
.join(Comments, cls.comment_id == Comments.id)
|
|
599
|
+
.filter(Comments.created_at > time)
|
|
600
|
+
.all()
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
return reactions
|
|
@@ -84,7 +84,24 @@ def update_metrics(
|
|
|
84
84
|
|
|
85
85
|
def track_request(name=None):
|
|
86
86
|
"""
|
|
87
|
-
Decorator to track requests metrics
|
|
87
|
+
Decorator to track requests/responses metrics
|
|
88
|
+
|
|
89
|
+
On entering the decorated function, it registers the time for the request
|
|
90
|
+
and saves the passed log content.
|
|
91
|
+
On exiting the decorated function, it registers the time for the response
|
|
92
|
+
and saves the generated response.
|
|
93
|
+
|
|
94
|
+
Use it to decorate server endpoints that generate a llm response
|
|
95
|
+
as in the following example:
|
|
96
|
+
|
|
97
|
+
>>> @app.post("/analyze", response_model=Response)
|
|
98
|
+
>>> @track_request()
|
|
99
|
+
>>> async def analyze_log(build_log)
|
|
100
|
+
>>> pass
|
|
101
|
+
|
|
102
|
+
Warning: the decorators' order is important!
|
|
103
|
+
The function returned by the *track_request* decorator is the
|
|
104
|
+
server API function we want to be called by FastAPI.
|
|
88
105
|
"""
|
|
89
106
|
|
|
90
107
|
def decorator(f):
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import datetime
|
|
2
|
-
from typing import Optional, Union
|
|
2
|
+
from typing import Optional, Union, Dict
|
|
3
3
|
|
|
4
4
|
import numpy
|
|
5
5
|
import matplotlib
|
|
6
6
|
import matplotlib.figure
|
|
7
7
|
import matplotlib.pyplot
|
|
8
8
|
|
|
9
|
+
from matplotlib.pyplot import cm
|
|
9
10
|
from logdetective.server import models
|
|
10
|
-
from logdetective.server.database.models import
|
|
11
|
+
from logdetective.server.database.models import (
|
|
12
|
+
AnalyzeRequestMetrics,
|
|
13
|
+
EndpointType,
|
|
14
|
+
Reactions,
|
|
15
|
+
)
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
class Definition:
|
|
@@ -145,13 +150,19 @@ def _add_bar_chart(
|
|
|
145
150
|
ax.grid(True, alpha=0.3)
|
|
146
151
|
|
|
147
152
|
|
|
148
|
-
def _add_line_chart(
|
|
149
|
-
ax: matplotlib.figure.Axes,
|
|
150
|
-
|
|
153
|
+
def _add_line_chart( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
|
|
154
|
+
ax: matplotlib.figure.Axes,
|
|
155
|
+
timestamps: numpy.array,
|
|
156
|
+
values: numpy.array,
|
|
157
|
+
label: str,
|
|
158
|
+
color: str = "red",
|
|
159
|
+
set_label: bool = True,
|
|
160
|
+
):
|
|
151
161
|
"""Add a red line chart"""
|
|
152
|
-
ax.plot(timestamps, values, "
|
|
153
|
-
|
|
154
|
-
|
|
162
|
+
ax.plot(timestamps, values, color=color, linestyle="-", linewidth=2, label=label)
|
|
163
|
+
if set_label:
|
|
164
|
+
ax.set_ylabel(label, color=color)
|
|
165
|
+
ax.tick_params(axis="y", labelcolor=color)
|
|
155
166
|
|
|
156
167
|
|
|
157
168
|
def requests_per_time(
|
|
@@ -249,7 +260,9 @@ def average_time_per_responses( # pylint: disable=too-many-locals
|
|
|
249
260
|
)
|
|
250
261
|
|
|
251
262
|
fig, ax1 = matplotlib.pyplot.subplots(figsize=(12, 6))
|
|
252
|
-
_add_bar_chart(
|
|
263
|
+
_add_bar_chart(
|
|
264
|
+
ax1, plot_def, timestamps, average_time, "average response time (seconds)"
|
|
265
|
+
)
|
|
253
266
|
|
|
254
267
|
responses_average_length = (
|
|
255
268
|
AnalyzeRequestMetrics.get_responses_average_length_in_period(
|
|
@@ -279,3 +292,138 @@ def average_time_per_responses( # pylint: disable=too-many-locals
|
|
|
279
292
|
matplotlib.pyplot.tight_layout()
|
|
280
293
|
|
|
281
294
|
return fig
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _collect_emoji_data(
|
|
298
|
+
start_time: datetime.datetime, plot_def: Definition
|
|
299
|
+
) -> Dict[str, Dict[datetime.datetime, int]]:
|
|
300
|
+
"""Collect and organize emoji feedback data
|
|
301
|
+
|
|
302
|
+
Counts all emojis given to logdetective comments created since start_time.
|
|
303
|
+
Collect counts in time accordingly to the plot definition.
|
|
304
|
+
"""
|
|
305
|
+
reactions = Reactions.get_since(start_time)
|
|
306
|
+
reactions_values_dict: Dict[str, Dict] = {}
|
|
307
|
+
for comment_created_at, reaction in reactions:
|
|
308
|
+
comment_created_at_formatted = comment_created_at.strptime(
|
|
309
|
+
comment_created_at.strftime(plot_def.time_format), plot_def.time_format
|
|
310
|
+
)
|
|
311
|
+
if reaction.reaction_type in reactions_values_dict:
|
|
312
|
+
reaction_values_dict = reactions_values_dict[reaction.reaction_type]
|
|
313
|
+
if comment_created_at_formatted in reaction_values_dict:
|
|
314
|
+
reaction_values_dict[comment_created_at_formatted] += reaction.count
|
|
315
|
+
else:
|
|
316
|
+
reaction_values_dict[comment_created_at_formatted] = reaction.count
|
|
317
|
+
else:
|
|
318
|
+
reaction_values_dict = {comment_created_at_formatted: reaction.count}
|
|
319
|
+
reactions_values_dict.update({reaction.reaction_type: reaction_values_dict})
|
|
320
|
+
|
|
321
|
+
return reactions_values_dict
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _plot_emoji_data( # pylint: disable=too-many-locals
|
|
325
|
+
ax: matplotlib.figure.Axes,
|
|
326
|
+
reactions_values_dict: Dict[str, Dict[datetime.datetime, int]],
|
|
327
|
+
plot_def: Definition,
|
|
328
|
+
start_time: datetime.datetime,
|
|
329
|
+
end_time: datetime.datetime,
|
|
330
|
+
):
|
|
331
|
+
"""Plot each emoji's data on its own axis."""
|
|
332
|
+
emoji_lines = {}
|
|
333
|
+
emoji_labels = {}
|
|
334
|
+
|
|
335
|
+
# Find global min and max y values to set consistent scale
|
|
336
|
+
all_counts = []
|
|
337
|
+
for emoji, dict_counts in reactions_values_dict.items():
|
|
338
|
+
timestamps, counts = create_time_series_arrays(
|
|
339
|
+
dict_counts, plot_def, start_time, end_time
|
|
340
|
+
)
|
|
341
|
+
all_counts.extend(counts)
|
|
342
|
+
|
|
343
|
+
colors = [cm.viridis(i) for i in numpy.linspace(0, 1, len(reactions_values_dict))] # pylint: disable=no-member
|
|
344
|
+
|
|
345
|
+
first_emoji = True
|
|
346
|
+
for i, (emoji, dict_counts) in enumerate(reactions_values_dict.items()):
|
|
347
|
+
timestamps, counts = create_time_series_arrays(
|
|
348
|
+
dict_counts, plot_def, start_time, end_time
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
if first_emoji:
|
|
352
|
+
current_ax = ax
|
|
353
|
+
first_emoji = False
|
|
354
|
+
else:
|
|
355
|
+
current_ax = ax.twinx()
|
|
356
|
+
current_ax.spines["right"].set_position(("outward", 60 * (i - 1)))
|
|
357
|
+
|
|
358
|
+
_add_line_chart(current_ax, timestamps, counts, f"{emoji}", colors[i], False)
|
|
359
|
+
emoji_lines[emoji], emoji_labels[emoji] = current_ax.get_legend_handles_labels()
|
|
360
|
+
|
|
361
|
+
# Set the same y-limits for all axes
|
|
362
|
+
current_ax.set_ylim(0, max(all_counts) * 1.1)
|
|
363
|
+
|
|
364
|
+
# Only show y-ticks on the first axis to avoid clutter
|
|
365
|
+
if 0 < i < len(reactions_values_dict):
|
|
366
|
+
current_ax.set_yticks([])
|
|
367
|
+
|
|
368
|
+
return emoji_lines, emoji_labels
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def emojis_per_time(
|
|
372
|
+
period_of_time: models.TimePeriod,
|
|
373
|
+
end_time: Optional[datetime.datetime] = None,
|
|
374
|
+
) -> matplotlib.figure.Figure:
|
|
375
|
+
"""
|
|
376
|
+
Generate a visualization of overall emoji feedback
|
|
377
|
+
over a specified time period.
|
|
378
|
+
|
|
379
|
+
This function creates a multiple-axis plot showing
|
|
380
|
+
a line chart for every found emoji
|
|
381
|
+
|
|
382
|
+
The time intervals are determined by the provided TimePeriod object, which defines
|
|
383
|
+
the granularity and formatting of the time axis.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
period_of_time: A TimePeriod object that defines the time period and interval
|
|
387
|
+
for the analysis (e.g., hourly, daily, weekly)
|
|
388
|
+
end_time: The end time for the analysis period. If None, defaults to the current
|
|
389
|
+
UTC time
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
A matplotlib Figure object containing the generated visualization
|
|
393
|
+
"""
|
|
394
|
+
plot_def = Definition(period_of_time)
|
|
395
|
+
end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
|
|
396
|
+
start_time = period_of_time.get_period_start_time(end_time)
|
|
397
|
+
reactions_values_dict = _collect_emoji_data(start_time, plot_def)
|
|
398
|
+
|
|
399
|
+
fig, ax = matplotlib.pyplot.subplots(figsize=(12, 6))
|
|
400
|
+
|
|
401
|
+
emoji_lines, emoji_labels = _plot_emoji_data(
|
|
402
|
+
ax, reactions_values_dict, plot_def, start_time, end_time
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
matplotlib.pyplot.title(
|
|
406
|
+
f"Emoji feedback ({start_time.strftime(plot_def.time_format)} "
|
|
407
|
+
f"to {end_time.strftime(plot_def.time_format)})"
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
all_lines = []
|
|
411
|
+
for lines in emoji_lines.values():
|
|
412
|
+
all_lines.extend(lines)
|
|
413
|
+
all_labels = []
|
|
414
|
+
for labels in emoji_labels.values():
|
|
415
|
+
all_labels.extend(labels)
|
|
416
|
+
|
|
417
|
+
ax.legend(all_lines, all_labels, loc="upper left")
|
|
418
|
+
ax.set_xlabel("Time")
|
|
419
|
+
ax.set_ylabel("Count")
|
|
420
|
+
|
|
421
|
+
# Format x-axis
|
|
422
|
+
ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter(plot_def.time_format))
|
|
423
|
+
ax.xaxis.set_major_locator(plot_def.locator)
|
|
424
|
+
ax.tick_params(axis="x", labelrotation=45)
|
|
425
|
+
ax.grid(True, alpha=0.3)
|
|
426
|
+
|
|
427
|
+
matplotlib.pyplot.tight_layout()
|
|
428
|
+
|
|
429
|
+
return fig
|
|
@@ -158,8 +158,8 @@ async def analyze_log(
|
|
|
158
158
|
return Response(explanation=response, response_certainty=certainty)
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
@track_request()
|
|
162
161
|
@app.post("/analyze/staged", response_model=StagedResponse)
|
|
162
|
+
@track_request()
|
|
163
163
|
async def analyze_log_staged(
|
|
164
164
|
build_log: BuildLog, http_session: aiohttp.ClientSession = Depends(get_http_session)
|
|
165
165
|
):
|
|
@@ -420,6 +420,7 @@ class Plot(str, Enum):
|
|
|
420
420
|
|
|
421
421
|
REQUESTS = "requests"
|
|
422
422
|
RESPONSES = "responses"
|
|
423
|
+
EMOJIS = "emojis"
|
|
423
424
|
BOTH = ""
|
|
424
425
|
|
|
425
426
|
|
|
@@ -450,12 +451,16 @@ async def get_metrics(
|
|
|
450
451
|
period_since_now, endpoint_type
|
|
451
452
|
)
|
|
452
453
|
return _svg_figure_response(fig)
|
|
454
|
+
if plot == Plot.EMOJIS:
|
|
455
|
+
fig = plot_engine.emojis_per_time(period_since_now)
|
|
456
|
+
return _svg_figure_response(fig)
|
|
453
457
|
# BOTH
|
|
454
458
|
fig_requests = plot_engine.requests_per_time(period_since_now, endpoint_type)
|
|
455
459
|
fig_responses = plot_engine.average_time_per_responses(
|
|
456
460
|
period_since_now, endpoint_type
|
|
457
461
|
)
|
|
458
|
-
|
|
462
|
+
fig_emojis = plot_engine.emojis_per_time(period_since_now)
|
|
463
|
+
return _multiple_svg_figures_response([fig_requests, fig_responses, fig_emojis])
|
|
459
464
|
|
|
460
465
|
descriptions = {
|
|
461
466
|
Plot.REQUESTS: (
|
|
@@ -466,6 +471,10 @@ async def get_metrics(
|
|
|
466
471
|
"Show statistics for responses given in the specified period of time "
|
|
467
472
|
f"for the /{endpoint_type.value} API endpoint."
|
|
468
473
|
),
|
|
474
|
+
Plot.EMOJIS: (
|
|
475
|
+
"Show statistics for emoji feedback in the specified period of time "
|
|
476
|
+
f"for the /{endpoint_type.value} API endpoint."
|
|
477
|
+
),
|
|
469
478
|
Plot.BOTH: (
|
|
470
479
|
"Show statistics for requests and responses in the given period of time "
|
|
471
480
|
f"for the /{endpoint_type.value} API endpoint."
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/templates/gitlab_full_comment.md.j2
RENAMED
|
File without changes
|
{logdetective-0.10.0 → logdetective-0.11.1}/logdetective/server/templates/gitlab_short_comment.md.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|