logdetective 0.10.0__py3-none-any.whl → 0.11.1__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.
@@ -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
@@ -115,7 +115,7 @@ def we_give_up(details: backoff._typing.Details):
115
115
 
116
116
 
117
117
  @backoff.on_exception(
118
- backoff.expo,
118
+ lambda: backoff.constant([10, 30, 120]),
119
119
  aiohttp.ClientResponseError,
120
120
  max_tries=3,
121
121
  giveup=should_we_giveup,
@@ -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 AnalyzeRequestMetrics, EndpointType
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, timestamps: numpy.array, values: numpy.array, label: str
150
- ) -> None:
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, "r-", linewidth=2, label=label)
153
- ax.set_ylabel(label, color="red")
154
- ax.tick_params(axis="y", labelcolor="red")
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(ax1, plot_def, timestamps, average_time, "average response time (seconds)")
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
- return _multiple_svg_figures_response([fig_requests, fig_responses])
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."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: logdetective
3
- Version: 0.10.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 responses statistics
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
 
@@ -12,20 +12,20 @@ logdetective/server/config.py,sha256=S2kuvzEo801Kq0vJpRr2fVxSqPghg9kW1L0Ml2yH8Zk
12
12
  logdetective/server/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  logdetective/server/database/base.py,sha256=1mcjEbhwLl4RalvT3oy6XVctjJoWIW3H9aI_sMWJBK8,1728
14
14
  logdetective/server/database/models/__init__.py,sha256=xy2hkygyw6_87zPKkG20i7g7_LXTGR__PUeojhbvv94,496
15
- logdetective/server/database/models/merge_request_jobs.py,sha256=5WOjJZZYaNMOlegB7Aty96k3NTCutcLP3WW8Yc9sENs,18105
15
+ logdetective/server/database/models/merge_request_jobs.py,sha256=hw88wV1-3x7i53sX7ZotKClc6OsH1njPpbRSZofnqr4,18670
16
16
  logdetective/server/database/models/metrics.py,sha256=yl9fS4IPVFWDeFvPAxO6zOVu6oLF319ApvVLAgnD5yU,13928
17
17
  logdetective/server/emoji.py,sha256=g9GtMChwznD8g1xonsh-I_3xqRn6LBeg3sjPJWcI0Yg,3333
18
18
  logdetective/server/gitlab.py,sha256=fpJp28YsvHvm4DjrvzrgamLk31Fo5UyvT6GNWway9KM,15227
19
- logdetective/server/llm.py,sha256=AXOqPbx3NJJpf2lOym3EYuVc2Uaf5UUORVQy8Nh2xdM,8516
20
- logdetective/server/metric.py,sha256=Jf5s_C464VQ2BRlHxq8CjjV7yJ9ZAE0ubnrYF6xr914,3352
19
+ logdetective/server/llm.py,sha256=YNpRv1PFd6V78rMiFXI2KCF8Rc3vMSTIHagKXDfbci8,8543
20
+ logdetective/server/metric.py,sha256=B3ew_qSmtEMj6xl-FoOtS4F_bkplp-shhtfHF1cG_Io,4010
21
21
  logdetective/server/models.py,sha256=eNEB3WJWeZ9Pe6qsmTKQwAE8wu8u51OwLILzV9__YJM,11248
22
- logdetective/server/plot.py,sha256=B2rOngqx7g-Z3NfttboTip3frkypdF1H7FhK8vh45mE,9655
23
- logdetective/server/server.py,sha256=7E0x9t1MHICXp6sMgc5Xj3IgbVmYzAvPSJ__brqf5fI,17709
22
+ logdetective/server/plot.py,sha256=eZs4r9gua-nW3yymSMIz1leL9mb4QKlh6FJZSeOfZ5M,14872
23
+ logdetective/server/server.py,sha256=9shFgRkWcJVM2L7HHoQBMCfKuJamh2L4tC96duFPEOA,18127
24
24
  logdetective/server/templates/gitlab_full_comment.md.j2,sha256=DQZ2WVFedpuXI6znbHIW4wpF9BmFS8FaUkowh8AnGhE,1627
25
25
  logdetective/server/templates/gitlab_short_comment.md.j2,sha256=fzScpayv2vpRLczP_0O0YxtA8rsKvR6gSv4ntNdWb98,1443
26
26
  logdetective/utils.py,sha256=hdExAC8FtDIxvdgIq-Ro6LVM-JZ-k_UofaMzaDAHvzM,6088
27
- logdetective-0.10.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
28
- logdetective-0.10.0.dist-info/METADATA,sha256=7ObUbKaJBF-dUWDGZlLlb-I9xGb0se6nSXS5B78vwDk,16340
29
- logdetective-0.10.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
30
- logdetective-0.10.0.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
31
- logdetective-0.10.0.dist-info/RECORD,,
27
+ logdetective-0.11.1.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
28
+ logdetective-0.11.1.dist-info/METADATA,sha256=JroEbY8u0mIDMdkUlfA8oeDdGo1TWnfXkLJZenvCBEo,16403
29
+ logdetective-0.11.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
30
+ logdetective-0.11.1.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
31
+ logdetective-0.11.1.dist-info/RECORD,,