logdetective 0.10.0__py3-none-any.whl → 0.11.2__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
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import asyncio
3
3
  import json
4
+ import random
4
5
  from typing import List, Tuple, Dict, Any, Union
5
6
 
6
7
  import backoff
@@ -102,7 +103,7 @@ def should_we_giveup(exc: aiohttp.ClientResponseError) -> bool:
102
103
  > a truthy value if the exception should not be retried
103
104
  """
104
105
  LOG.info("Should we give up on retrying error %s", exc)
105
- return exc.status < 500
106
+ return exc.status < 400
106
107
 
107
108
 
108
109
  def we_give_up(details: backoff._typing.Details):
@@ -110,14 +111,16 @@ def we_give_up(details: backoff._typing.Details):
110
111
  retries didn't work (or we got a different exc)
111
112
  we give up and raise proper 500 for our API endpoint
112
113
  """
114
+ LOG.error("Last exception: %s", details["exception"])
113
115
  LOG.error("Inference error: %s", details["args"])
114
116
  raise HTTPException(500, "Request to the inference API failed")
115
117
 
116
118
 
117
119
  @backoff.on_exception(
118
- backoff.expo,
120
+ lambda: backoff.constant([10, 30, 120]),
119
121
  aiohttp.ClientResponseError,
120
- max_tries=3,
122
+ max_tries=4, # 4 tries and 3 retries
123
+ jitter=lambda wait_gen_value: random.uniform(wait_gen_value, wait_gen_value + 30),
121
124
  giveup=should_we_giveup,
122
125
  raise_on_giveup=False,
123
126
  on_giveup=we_give_up,
@@ -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.2
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
@@ -48,14 +48,16 @@ Log Detective
48
48
 
49
49
  [PyPI Releases]: https://pypi.org/project/logdetective/#history
50
50
 
51
- A Python tool to analyze logs using a Language Model (LLM) and Drain template miner.
51
+ A tool, service and RHEL process integration to analyze logs using a Large Language Model (LLM) and a [Drain template miner](https://github.com/logpai/Drain3).
52
+
53
+ The service that explains logs is available here: https://logdetective.com/explain
52
54
 
53
55
  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).
54
56
 
55
57
  Installation
56
58
  ------------
57
59
 
58
- **Fedora 40+**
60
+ **Fedora 41+**
59
61
 
60
62
  dnf install logdetective
61
63
 
@@ -70,11 +72,12 @@ First, ensure that the necessary dependencies for the `llama-cpp-python` project
70
72
 
71
73
  Then, install the `logdetective` project using pip:
72
74
 
73
- # then install logdetective project
74
75
  pip install logdetective
75
76
 
76
77
  **Local repository install**
77
78
 
79
+ Clone this repository and install with pip:
80
+
78
81
  pip install .
79
82
 
80
83
  Usage
@@ -111,14 +114,14 @@ Example of altered prompts:
111
114
  logdetective https://kojipkgs.fedoraproject.org//work/tasks/3367/131313367/build.log --prompts ~/my-prompts.yml
112
115
 
113
116
 
114
- Note that streaming with some models (notably Meta-Llama-3 is broken) is broken and can be workarounded by `no-stream` option:
117
+ Note that streaming with some models (notably Meta-Llama-3 is broken) is broken and can be worked around by `no-stream` option:
115
118
 
116
119
  logdetective https://example.com/logs.txt --model QuantFactory/Meta-Llama-3-8B-Instruct-GGUF --no-stream
117
120
 
118
121
 
119
122
  Real Example
120
123
  ------------
121
- Let's have a look at a real world example. Log Detective can work with any logs though we optimize it for build logs.
124
+ Let's have a look at a real world example. Log Detective can work with any logs though we optimize it for RPM build logs.
122
125
 
123
126
  We're going to analyze a failed build of a python-based library that happened in Fedora Koji buildsystem:
124
127
  ```
@@ -184,8 +187,13 @@ Contributing
184
187
  ------------
185
188
 
186
189
  Contributions are welcome! Please submit a pull request if you have any improvements or new features to add. Make sure your changes pass all existing tests before submitting.
190
+ For bigger code changes, please consult us first by creating an issue.
191
+
192
+ We are always looking for more annotated snippets that will increase the quality of Log Detective's results. The contributions happen in our website: https://logdetective.com/
193
+
194
+ Log Detective performs several inference queries while evaluating a log file. Prompts are stored in a separate file (more info below: https://github.com/fedora-copr/logdetective?tab=readme-ov-file#system-prompts). If you have an idea for improvements to our prompts, please open a PR and we'd happy to test it out.
187
195
 
188
- To develop logdetective, you should fork this repository, clone your fork, and install dependencies using pip:
196
+ To develop Log Detective, you should fork this repository, clone your fork, and install dependencies using pip:
189
197
 
190
198
  git clone https://github.com/yourusername/logdetective.git
191
199
  cd logdetective
@@ -358,7 +366,7 @@ using either a browser, the `curl` or the `http` command (provided by the `httpi
358
366
 
359
367
  When no time period is specified, the query defaults to the last 2 days:
360
368
 
361
- You can view requests and responses statistics
369
+ You can view requests, responses and emojis statistics
362
370
  - for the `/analyze` endpoint at http://localhost:8080/metrics/analyze
363
371
  - for the `/analyze-staged` endpoint at http://localhost:8080/metrics/analyze-staged.
364
372
  - for the requests coming from gitlab: http://localhost:8080/metrics/analyze-gitlab.
@@ -370,6 +378,7 @@ You can retrieve single svg images at the following endpoints:
370
378
  - http://localhost:8080/metrics/analyze-staged/responses
371
379
  - http://localhost:8080/metrics/analyze-gitlab/requests
372
380
  - http://localhost:8080/metrics/analyze-gitlab/responses
381
+ - http://localhost:8080/metrics/analyze-gitlab/emojis
373
382
 
374
383
  Examples:
375
384
 
@@ -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=JtSCZj8SLnoyTCUdhA0TwcsMZfmHFFru2bJ9txI3GuU,8727
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.2.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
28
+ logdetective-0.11.2.dist-info/METADATA,sha256=BVRCWRVzlm-Aa0b51d0ZCbKz2ty1htTzHzX9XGiXALI,17137
29
+ logdetective-0.11.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
30
+ logdetective-0.11.2.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
31
+ logdetective-0.11.2.dist-info/RECORD,,