logdetective 2.12.0__py3-none-any.whl → 3.0.0__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/logdetective.py +3 -23
- logdetective/server/database/base.py +1 -1
- logdetective/server/database/models/metrics.py +7 -4
- logdetective/server/metric.py +200 -7
- logdetective/server/models.py +12 -0
- logdetective/server/server.py +45 -75
- {logdetective-2.12.0.dist-info → logdetective-3.0.0.dist-info}/METADATA +12 -11
- {logdetective-2.12.0.dist-info → logdetective-3.0.0.dist-info}/RECORD +11 -12
- {logdetective-2.12.0.dist-info → logdetective-3.0.0.dist-info}/WHEEL +1 -1
- logdetective/server/plot.py +0 -432
- {logdetective-2.12.0.dist-info → logdetective-3.0.0.dist-info}/entry_points.txt +0 -0
- {logdetective-2.12.0.dist-info → logdetective-3.0.0.dist-info}/licenses/LICENSE +0 -0
logdetective/logdetective.py
CHANGED
|
@@ -41,31 +41,15 @@ def setup_args():
|
|
|
41
41
|
)
|
|
42
42
|
parser.add_argument(
|
|
43
43
|
"-F",
|
|
44
|
-
"--
|
|
44
|
+
"--filename-suffix",
|
|
45
45
|
help="Suffix of the model file name to be retrieved from Hugging Face.\
|
|
46
46
|
Makes sense only if the model is specified with Hugging Face name.",
|
|
47
47
|
default="Q4_K.gguf",
|
|
48
48
|
)
|
|
49
49
|
parser.add_argument("-n", "--no-stream", action="store_true")
|
|
50
|
-
parser.add_argument(
|
|
51
|
-
"-S",
|
|
52
|
-
"--summarizer",
|
|
53
|
-
type=str,
|
|
54
|
-
default="drain",
|
|
55
|
-
help="DISABLED: LLM summarization option was removed. \
|
|
56
|
-
Argument is kept for backward compatibility only.",
|
|
57
|
-
)
|
|
58
|
-
parser.add_argument(
|
|
59
|
-
"-N",
|
|
60
|
-
"--n_lines",
|
|
61
|
-
type=int,
|
|
62
|
-
default=None,
|
|
63
|
-
help="DISABLED: LLM summarization option was removed. \
|
|
64
|
-
Argument is kept for backward compatibility only.",
|
|
65
|
-
)
|
|
66
50
|
parser.add_argument(
|
|
67
51
|
"-C",
|
|
68
|
-
"--
|
|
52
|
+
"--n-clusters",
|
|
69
53
|
type=int,
|
|
70
54
|
default=8,
|
|
71
55
|
help="Number of clusters for Drain to organize log chunks into.\
|
|
@@ -86,7 +70,7 @@ def setup_args():
|
|
|
86
70
|
help="Temperature for inference.",
|
|
87
71
|
)
|
|
88
72
|
parser.add_argument(
|
|
89
|
-
"--
|
|
73
|
+
"--skip-snippets",
|
|
90
74
|
type=str,
|
|
91
75
|
default=f"{os.path.dirname(__file__)}/skip_snippets.yml",
|
|
92
76
|
help="Path to patterns for skipping snippets.",
|
|
@@ -105,10 +89,6 @@ async def run(): # pylint: disable=too-many-statements,too-many-locals,too-many
|
|
|
105
89
|
sys.stderr.write("Error: --quiet and --verbose is mutually exclusive.\n")
|
|
106
90
|
sys.exit(2)
|
|
107
91
|
|
|
108
|
-
# Emit warning about use of discontinued args
|
|
109
|
-
if args.n_lines or args.summarizer != "drain":
|
|
110
|
-
LOG.warning("LLM based summarization was removed. Drain will be used instead.")
|
|
111
|
-
|
|
112
92
|
# Logging facility setup
|
|
113
93
|
log_level = logging.INFO
|
|
114
94
|
if args.verbose >= 1:
|
|
@@ -22,7 +22,7 @@ sqlalchemy_echo = getenv("SQLALCHEMY_ECHO", "False").lower() in (
|
|
|
22
22
|
"y",
|
|
23
23
|
"1",
|
|
24
24
|
)
|
|
25
|
-
engine = create_async_engine(get_pg_url(), echo=sqlalchemy_echo)
|
|
25
|
+
engine = create_async_engine(get_pg_url(), echo=sqlalchemy_echo, pool_pre_ping=True)
|
|
26
26
|
SessionFactory = async_sessionmaker(autoflush=True, bind=engine) # pylint: disable=invalid-name
|
|
27
27
|
|
|
28
28
|
|
|
@@ -314,10 +314,13 @@ class AnalyzeRequestMetrics(Base):
|
|
|
314
314
|
"time_range"
|
|
315
315
|
),
|
|
316
316
|
(
|
|
317
|
-
func.
|
|
318
|
-
func.
|
|
319
|
-
|
|
320
|
-
|
|
317
|
+
func.coalesce(
|
|
318
|
+
func.avg(
|
|
319
|
+
func.extract( # pylint: disable=not-callable
|
|
320
|
+
"epoch", cls.response_sent_at - cls.request_received_at
|
|
321
|
+
)
|
|
322
|
+
),
|
|
323
|
+
0
|
|
321
324
|
)
|
|
322
325
|
).label("average_response_seconds"),
|
|
323
326
|
)
|
logdetective/server/metric.py
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
from collections import defaultdict
|
|
2
3
|
import datetime
|
|
3
|
-
|
|
4
|
-
from typing import Optional, Union
|
|
4
|
+
from typing import Optional, Union, Dict
|
|
5
5
|
from functools import wraps
|
|
6
6
|
|
|
7
7
|
import aiohttp
|
|
8
|
-
|
|
8
|
+
import numpy
|
|
9
9
|
from starlette.responses import StreamingResponse
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
from logdetective.remote_log import RemoteLog
|
|
12
12
|
from logdetective.server.config import LOG
|
|
13
13
|
from logdetective.server.compressors import LLMResponseCompressor, RemoteLogCompressor
|
|
14
|
-
from logdetective.server.
|
|
14
|
+
from logdetective.server.models import (
|
|
15
|
+
TimePeriod,
|
|
16
|
+
MetricTimeSeries,
|
|
17
|
+
StagedResponse,
|
|
18
|
+
Response,
|
|
19
|
+
Explanation,
|
|
20
|
+
)
|
|
21
|
+
from logdetective.server.database.models import EndpointType, AnalyzeRequestMetrics, Reactions
|
|
15
22
|
from logdetective.server.exceptions import LogDetectiveMetricsError
|
|
16
23
|
|
|
17
24
|
|
|
@@ -47,7 +54,7 @@ async def add_new_metrics(
|
|
|
47
54
|
|
|
48
55
|
async def update_metrics(
|
|
49
56
|
metrics_id: int,
|
|
50
|
-
response: Union[
|
|
57
|
+
response: Union[Response, StagedResponse, StreamingResponse],
|
|
51
58
|
sent_at: Optional[datetime.datetime] = None,
|
|
52
59
|
) -> None:
|
|
53
60
|
"""Update a database metric entry for a received request,
|
|
@@ -71,7 +78,7 @@ async def update_metrics(
|
|
|
71
78
|
)
|
|
72
79
|
response_length = None
|
|
73
80
|
if hasattr(response, "explanation") and isinstance(
|
|
74
|
-
response.explanation,
|
|
81
|
+
response.explanation, Explanation
|
|
75
82
|
):
|
|
76
83
|
response_length = len(response.explanation.text)
|
|
77
84
|
response_certainty = (
|
|
@@ -125,3 +132,189 @@ def track_request(name=None):
|
|
|
125
132
|
raise NotImplementedError("An async coroutine is needed")
|
|
126
133
|
|
|
127
134
|
return decorator
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# TODO: Refactor aggregation to use database operations, instead of timestamp formatting # pylint: disable=fixme
|
|
138
|
+
class TimeDefinition:
|
|
139
|
+
"""Define time format details, given a time period."""
|
|
140
|
+
|
|
141
|
+
def __init__(self, time_period: TimePeriod):
|
|
142
|
+
self.time_period = time_period
|
|
143
|
+
self.days_diff = time_period.get_time_period().days
|
|
144
|
+
if self.time_period.hours:
|
|
145
|
+
self._time_format = "%Y-%m-%d %H"
|
|
146
|
+
self._time_delta = datetime.timedelta(hours=1)
|
|
147
|
+
elif self.time_period.days:
|
|
148
|
+
self._time_format = "%Y-%m-%d"
|
|
149
|
+
self._time_delta = datetime.timedelta(days=1)
|
|
150
|
+
elif self.time_period.weeks:
|
|
151
|
+
self._time_format = "%Y-%m-%d"
|
|
152
|
+
self._time_delta = datetime.timedelta(weeks=1)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def time_format(self):
|
|
156
|
+
# pylint: disable=missing-function-docstring
|
|
157
|
+
return self._time_format
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def time_delta(self):
|
|
161
|
+
# pylint: disable=missing-function-docstring
|
|
162
|
+
return self._time_delta
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def create_time_series_arrays(
|
|
166
|
+
values_dict: dict[datetime.datetime, int],
|
|
167
|
+
) -> tuple[list, list]:
|
|
168
|
+
"""Create time series arrays from a dictionary of values.
|
|
169
|
+
|
|
170
|
+
This function generates two aligned lists:
|
|
171
|
+
1. An array of timestamps from start_time to end_time
|
|
172
|
+
2. A corresponding array of values for each timestamp
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
values_dict: Dictionary mapping timestamps to their respective values
|
|
176
|
+
Returns:
|
|
177
|
+
A tuple containing:
|
|
178
|
+
- list: Array of timestamps
|
|
179
|
+
- list: Array of corresponding values
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
timestamps = []
|
|
183
|
+
values = []
|
|
184
|
+
|
|
185
|
+
for timestamp, count in values_dict.items():
|
|
186
|
+
timestamps.append(timestamp)
|
|
187
|
+
values.append(count)
|
|
188
|
+
|
|
189
|
+
return timestamps, numpy.nan_to_num(values).tolist()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def requests_per_time(
|
|
193
|
+
period_of_time: TimePeriod,
|
|
194
|
+
endpoint: EndpointType = EndpointType.ANALYZE,
|
|
195
|
+
end_time: Optional[datetime.datetime] = None,
|
|
196
|
+
) -> MetricTimeSeries:
|
|
197
|
+
"""
|
|
198
|
+
Get request counts over a specified time period.
|
|
199
|
+
|
|
200
|
+
The time intervals are determined by the provided TimePeriod object, which defines
|
|
201
|
+
the granularity.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
period_of_time: A TimePeriod object that defines the time period and interval
|
|
205
|
+
for the analysis (e.g., hourly, daily, weekly)
|
|
206
|
+
endpoint: One of the API endpoints
|
|
207
|
+
end_time: The end time for the analysis period. If None, defaults to the current
|
|
208
|
+
UTC time
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
A dictionary with timestamps and associated number of requests
|
|
212
|
+
"""
|
|
213
|
+
end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
|
|
214
|
+
start_time = period_of_time.get_period_start_time(end_time)
|
|
215
|
+
time_def = TimeDefinition(period_of_time)
|
|
216
|
+
requests_counts = await AnalyzeRequestMetrics.get_requests_in_period(
|
|
217
|
+
start_time, end_time, time_def.time_format, endpoint
|
|
218
|
+
)
|
|
219
|
+
timestamps, counts = create_time_series_arrays(requests_counts)
|
|
220
|
+
|
|
221
|
+
return MetricTimeSeries(metric="requests", timestamps=timestamps, values=counts)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def average_time_per_responses(
|
|
225
|
+
period_of_time: TimePeriod,
|
|
226
|
+
endpoint: EndpointType = EndpointType.ANALYZE,
|
|
227
|
+
end_time: Optional[datetime.datetime] = None,
|
|
228
|
+
) -> MetricTimeSeries:
|
|
229
|
+
"""
|
|
230
|
+
Get average response time and length over a specified time period.
|
|
231
|
+
|
|
232
|
+
The time intervals are determined by the provided TimePeriod object, which defines
|
|
233
|
+
the granularity.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
period_of_time: A TimePeriod object that defines the time period and interval
|
|
237
|
+
for the analysis (e.g., hourly, daily, weekly)
|
|
238
|
+
endpoint: One of the API endpoints
|
|
239
|
+
end_time: The end time for the analysis period. If None, defaults to the current
|
|
240
|
+
UTC time
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
A dictionary of timestamps and average response times
|
|
244
|
+
"""
|
|
245
|
+
end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
|
|
246
|
+
start_time = period_of_time.get_period_start_time(end_time)
|
|
247
|
+
time_def = TimeDefinition(period_of_time)
|
|
248
|
+
responses_average_time = (
|
|
249
|
+
await AnalyzeRequestMetrics.get_responses_average_time_in_period(
|
|
250
|
+
start_time, end_time, time_def.time_format, endpoint
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
timestamps, average_time = create_time_series_arrays(
|
|
254
|
+
responses_average_time,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return MetricTimeSeries(metric="avg_response_time", timestamps=timestamps, values=average_time)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
async def _collect_emoji_data(
|
|
261
|
+
start_time: datetime.datetime, time_def: TimeDefinition
|
|
262
|
+
) -> Dict[str, Dict[str, list]]:
|
|
263
|
+
"""Collect and organize emoji feedback data
|
|
264
|
+
|
|
265
|
+
For each reaction type, a dictionary is created with time stamps
|
|
266
|
+
as keys, and aggregate counts as values.
|
|
267
|
+
"""
|
|
268
|
+
reactions = await Reactions.get_since(start_time)
|
|
269
|
+
reaction_values: defaultdict[str, Dict] = defaultdict(lambda: defaultdict(int))
|
|
270
|
+
|
|
271
|
+
for comment_timestamp, reaction in reactions:
|
|
272
|
+
formatted_timestamp = comment_timestamp.strptime(
|
|
273
|
+
comment_timestamp.strftime(time_def.time_format), time_def.time_format
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
reaction_values[reaction.reaction_type][formatted_timestamp] += reaction.count
|
|
277
|
+
|
|
278
|
+
reaction_time_series = {
|
|
279
|
+
reaction_type: {
|
|
280
|
+
"timestamps": reaction_data.keys(),
|
|
281
|
+
"values": reaction_data.values(),
|
|
282
|
+
}
|
|
283
|
+
for reaction_type, reaction_data in reaction_values.items()
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return reaction_time_series
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def emojis_per_time(
|
|
290
|
+
period_of_time: TimePeriod,
|
|
291
|
+
end_time: Optional[datetime.datetime] = None,
|
|
292
|
+
) -> list[MetricTimeSeries]:
|
|
293
|
+
"""
|
|
294
|
+
Retrieve data of emoji feedback over time.
|
|
295
|
+
|
|
296
|
+
The time intervals are determined by the provided TimePeriod object, which defines
|
|
297
|
+
the granularity.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
period_of_time: A TimePeriod object that defines the time period and interval
|
|
301
|
+
for the analysis (e.g., hourly, daily, weekly)
|
|
302
|
+
end_time: The end time for the analysis period. If None, defaults to the current
|
|
303
|
+
UTC time
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
A list of `MetricTimeSeries` objects
|
|
307
|
+
"""
|
|
308
|
+
time_def = TimeDefinition(period_of_time)
|
|
309
|
+
end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
|
|
310
|
+
start_time = period_of_time.get_period_start_time(end_time)
|
|
311
|
+
reactions_values_dict = await _collect_emoji_data(start_time, time_def)
|
|
312
|
+
|
|
313
|
+
reaction_values: list[MetricTimeSeries] = []
|
|
314
|
+
for reaction, time_series in reactions_values_dict.items():
|
|
315
|
+
reaction_values.append(
|
|
316
|
+
MetricTimeSeries(
|
|
317
|
+
metric=f"emoji_{reaction}",
|
|
318
|
+
timestamps=time_series["timestamps"],
|
|
319
|
+
values=time_series["values"]))
|
|
320
|
+
return reaction_values
|
logdetective/server/models.py
CHANGED
|
@@ -401,3 +401,15 @@ class TimePeriod(BaseModel):
|
|
|
401
401
|
if time.tzinfo is None:
|
|
402
402
|
time = time.replace(tzinfo=datetime.timezone.utc)
|
|
403
403
|
return time - self.get_time_period()
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class MetricTimeSeries(BaseModel):
|
|
407
|
+
"""Recorded values of given metric"""
|
|
408
|
+
metric: str
|
|
409
|
+
timestamps: List[datetime.datetime]
|
|
410
|
+
values: List[float]
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class MetricResponse(BaseModel):
|
|
414
|
+
"""Requested metrics"""
|
|
415
|
+
time_series: List[MetricTimeSeries]
|
logdetective/server/server.py
CHANGED
|
@@ -5,12 +5,8 @@ from enum import Enum
|
|
|
5
5
|
from collections import defaultdict
|
|
6
6
|
from contextlib import asynccontextmanager
|
|
7
7
|
from typing import Annotated
|
|
8
|
-
from io import BytesIO
|
|
9
8
|
|
|
10
9
|
from aiolimiter import AsyncLimiter
|
|
11
|
-
import matplotlib
|
|
12
|
-
import matplotlib.figure
|
|
13
|
-
import matplotlib.pyplot
|
|
14
10
|
from koji import ClientSession
|
|
15
11
|
from gitlab import Gitlab
|
|
16
12
|
from fastapi import (
|
|
@@ -50,7 +46,14 @@ from logdetective.server.llm import (
|
|
|
50
46
|
perform_analysis_stream,
|
|
51
47
|
)
|
|
52
48
|
from logdetective.server.gitlab import process_gitlab_job_event
|
|
53
|
-
from logdetective.server.metric import
|
|
49
|
+
from logdetective.server.metric import (
|
|
50
|
+
track_request,
|
|
51
|
+
add_new_metrics,
|
|
52
|
+
update_metrics,
|
|
53
|
+
requests_per_time,
|
|
54
|
+
average_time_per_responses,
|
|
55
|
+
emojis_per_time
|
|
56
|
+
)
|
|
54
57
|
from logdetective.server.models import (
|
|
55
58
|
BuildLog,
|
|
56
59
|
Config,
|
|
@@ -62,8 +65,8 @@ from logdetective.server.models import (
|
|
|
62
65
|
StagedResponse,
|
|
63
66
|
TimePeriod,
|
|
64
67
|
ExtractorConfig,
|
|
68
|
+
MetricResponse,
|
|
65
69
|
)
|
|
66
|
-
from logdetective.server import plot as plot_engine
|
|
67
70
|
from logdetective.server.database.models import (
|
|
68
71
|
EndpointType,
|
|
69
72
|
Forge,
|
|
@@ -730,38 +733,6 @@ async def schedule_emoji_collection_for_mr(
|
|
|
730
733
|
del emoji_lookup[key]
|
|
731
734
|
|
|
732
735
|
|
|
733
|
-
def _svg_figure_response(fig: matplotlib.figure.Figure):
|
|
734
|
-
"""Create a response with the given svg figure."""
|
|
735
|
-
buf = BytesIO()
|
|
736
|
-
fig.savefig(buf, format="svg", bbox_inches="tight")
|
|
737
|
-
matplotlib.pyplot.close(fig)
|
|
738
|
-
|
|
739
|
-
buf.seek(0)
|
|
740
|
-
return StreamingResponse(
|
|
741
|
-
buf,
|
|
742
|
-
media_type="image/svg+xml",
|
|
743
|
-
headers={"Content-Disposition": "inline; filename=plot.svg"},
|
|
744
|
-
)
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
def _multiple_svg_figures_response(figures: list[matplotlib.figure.Figure]):
|
|
748
|
-
"""Create a response with multiple svg figures."""
|
|
749
|
-
svg_contents = []
|
|
750
|
-
for i, fig in enumerate(figures):
|
|
751
|
-
buf = BytesIO()
|
|
752
|
-
fig.savefig(buf, format="svg", bbox_inches="tight")
|
|
753
|
-
matplotlib.pyplot.close(fig)
|
|
754
|
-
buf.seek(0)
|
|
755
|
-
svg_contents.append(buf.read().decode("utf-8"))
|
|
756
|
-
|
|
757
|
-
html_content = "<html><body>\n"
|
|
758
|
-
for i, svg in enumerate(svg_contents):
|
|
759
|
-
html_content += f"<div id='figure-{i}'>\n{svg}\n</div>\n"
|
|
760
|
-
html_content += "</body></html>"
|
|
761
|
-
|
|
762
|
-
return BasicResponse(content=html_content, media_type="text/html")
|
|
763
|
-
|
|
764
|
-
|
|
765
736
|
class MetricRoute(str, Enum):
|
|
766
737
|
"""Routes for metrics"""
|
|
767
738
|
|
|
@@ -770,13 +741,13 @@ class MetricRoute(str, Enum):
|
|
|
770
741
|
ANALYZE_GITLAB_JOB = "analyze-gitlab"
|
|
771
742
|
|
|
772
743
|
|
|
773
|
-
class
|
|
774
|
-
"""Type of
|
|
744
|
+
class MetricType(str, Enum):
|
|
745
|
+
"""Type of metric retrieved"""
|
|
775
746
|
|
|
776
747
|
REQUESTS = "requests"
|
|
777
748
|
RESPONSES = "responses"
|
|
778
749
|
EMOJIS = "emojis"
|
|
779
|
-
|
|
750
|
+
ALL = "all"
|
|
780
751
|
|
|
781
752
|
|
|
782
753
|
ROUTE_TO_ENDPOINT_TYPES = {
|
|
@@ -786,58 +757,57 @@ ROUTE_TO_ENDPOINT_TYPES = {
|
|
|
786
757
|
}
|
|
787
758
|
|
|
788
759
|
|
|
789
|
-
@app.get("/metrics/{route}/",
|
|
790
|
-
@app.get("/metrics/{route}/{
|
|
760
|
+
@app.get("/metrics/{route}/", response_model=MetricResponse)
|
|
761
|
+
@app.get("/metrics/{route}/{metric_type}", response_model=MetricResponse)
|
|
791
762
|
async def get_metrics(
|
|
792
763
|
route: MetricRoute,
|
|
793
|
-
|
|
764
|
+
metric_type: MetricType = MetricType.ALL,
|
|
794
765
|
period_since_now: TimePeriod = Depends(TimePeriod),
|
|
795
766
|
):
|
|
796
|
-
"""Get an handler
|
|
767
|
+
"""Get an handler returning statistics for the specified endpoint and metric_type."""
|
|
797
768
|
endpoint_type = ROUTE_TO_ENDPOINT_TYPES[route]
|
|
798
769
|
|
|
799
|
-
async def handler():
|
|
800
|
-
"""
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
if plot == Plot.RESPONSES:
|
|
805
|
-
fig = await plot_engine.average_time_per_responses(
|
|
770
|
+
async def handler() -> MetricResponse:
|
|
771
|
+
"""Return statistics for the specified endpoint and metric type."""
|
|
772
|
+
statistics = []
|
|
773
|
+
if metric_type == MetricType.ALL:
|
|
774
|
+
statistics.append(await requests_per_time(
|
|
806
775
|
period_since_now, endpoint_type
|
|
807
|
-
)
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
period_since_now, endpoint_type
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
776
|
+
))
|
|
777
|
+
statistics.append(await average_time_per_responses(
|
|
778
|
+
period_since_now, endpoint_type
|
|
779
|
+
))
|
|
780
|
+
statistics.extend(await emojis_per_time(period_since_now))
|
|
781
|
+
return MetricResponse(time_series=statistics)
|
|
782
|
+
if metric_type == MetricType.REQUESTS:
|
|
783
|
+
statistics.append(await requests_per_time(period_since_now, endpoint_type))
|
|
784
|
+
elif metric_type == MetricType.RESPONSES:
|
|
785
|
+
statistics.append(await average_time_per_responses(
|
|
786
|
+
period_since_now, endpoint_type
|
|
787
|
+
))
|
|
788
|
+
elif metric_type == MetricType.EMOJIS:
|
|
789
|
+
statistics = await emojis_per_time(period_since_now)
|
|
790
|
+
return MetricResponse(time_series=statistics)
|
|
821
791
|
|
|
822
792
|
descriptions = {
|
|
823
|
-
|
|
824
|
-
"
|
|
793
|
+
MetricType.REQUESTS: (
|
|
794
|
+
"Get statistics for the requests received in the given period of time "
|
|
825
795
|
f"for the /{endpoint_type.value} API endpoint."
|
|
826
796
|
),
|
|
827
|
-
|
|
828
|
-
"
|
|
797
|
+
MetricType.RESPONSES: (
|
|
798
|
+
"Get statistics for responses given in the specified period of time "
|
|
829
799
|
f"for the /{endpoint_type.value} API endpoint."
|
|
830
800
|
),
|
|
831
|
-
|
|
832
|
-
"
|
|
801
|
+
MetricType.EMOJIS: (
|
|
802
|
+
"Get statistics for emoji feedback in the specified period of time "
|
|
833
803
|
f"for the /{endpoint_type.value} API endpoint."
|
|
834
804
|
),
|
|
835
|
-
|
|
836
|
-
"
|
|
805
|
+
MetricType.ALL: (
|
|
806
|
+
"Get statistics for requests and responses in the given period of time "
|
|
837
807
|
f"for the /{endpoint_type.value} API endpoint."
|
|
838
808
|
),
|
|
839
809
|
}
|
|
840
|
-
handler.__doc__ = descriptions[
|
|
810
|
+
handler.__doc__ = descriptions[metric_type]
|
|
841
811
|
|
|
842
812
|
return await handler()
|
|
843
813
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: logdetective
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
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
|
License-File: LICENSE
|
|
@@ -24,7 +24,7 @@ Provides-Extra: server
|
|
|
24
24
|
Provides-Extra: server-testing
|
|
25
25
|
Provides-Extra: testing
|
|
26
26
|
Requires-Dist: aiohttp (>=3.7.4,<4.0.0)
|
|
27
|
-
Requires-Dist: aiolimiter (>=1.0.0,<2.0.0) ; extra == "server"
|
|
27
|
+
Requires-Dist: aiolimiter (>=1.0.0,<2.0.0) ; extra == "server" or extra == "server-testing"
|
|
28
28
|
Requires-Dist: aioresponses (>=0.7.8,<0.8.0) ; extra == "testing"
|
|
29
29
|
Requires-Dist: alembic (>=1.13.3,<2.0.0) ; extra == "server" or extra == "server-testing"
|
|
30
30
|
Requires-Dist: asciidoc[testing] (>=10.2.1,<11.0.0) ; extra == "testing"
|
|
@@ -36,7 +36,6 @@ Requires-Dist: flexmock (>=0.12.2,<0.13.0) ; extra == "testing"
|
|
|
36
36
|
Requires-Dist: huggingface-hub (>=0.23.0,<1.4.0)
|
|
37
37
|
Requires-Dist: koji (>=1.35.0,<2.0.0) ; extra == "server" or extra == "server-testing"
|
|
38
38
|
Requires-Dist: llama-cpp-python (>0.2.56,!=0.2.86,<1.0.0)
|
|
39
|
-
Requires-Dist: matplotlib (>=3.8.4,<4.0.0) ; extra == "server" or extra == "server-testing"
|
|
40
39
|
Requires-Dist: numpy (>=1.26.0)
|
|
41
40
|
Requires-Dist: openai (>=1.82.1,<2.0.0) ; extra == "server" or extra == "server-testing"
|
|
42
41
|
Requires-Dist: pydantic (>=2.8.2,<3.0.0)
|
|
@@ -98,11 +97,13 @@ Usage
|
|
|
98
97
|
To analyze a log file, run the script with the following command line arguments:
|
|
99
98
|
- `file` (required): The path or URL of the log file to be analyzed.
|
|
100
99
|
- `--model` (optional, default: "Mistral-7B-Instruct-v0.3-GGUF"): The path or Hugging space name of the language model for analysis. For models from Hugging Face, write them as `namespace/repo_name`. As we are using LLama.cpp we want this to be in the `gguf` format. If the model is already on your machine it will skip the download.
|
|
101
|
-
- `--
|
|
102
|
-
- `--
|
|
103
|
-
- `--
|
|
104
|
-
- `--
|
|
105
|
-
- `--
|
|
100
|
+
- `--filename-suffix` (optional, default "Q4_K.gguf"): You can specify which suffix of the file to use. This option is applied when specifying model using the Hugging Face repository.
|
|
101
|
+
- `--n-clusters` (optional, default 8): Number of clusters for Drain to organize log chunks into. This only makes sense when you are summarizing with Drain.
|
|
102
|
+
- `--skip-snippets` Path to patterns for skipping snippets (in YAML).
|
|
103
|
+
- `--prompts PROMPTS` Path to prompt configuration file.
|
|
104
|
+
- `--temperature` Temperature for inference.
|
|
105
|
+
- `--skip-snippets` Path to patterns for skipping snippets.
|
|
106
|
+
- `--csgrep` Use csgrep to process the log.
|
|
106
107
|
|
|
107
108
|
Example usage:
|
|
108
109
|
|
|
@@ -112,9 +113,9 @@ Or if the log file is stored locally:
|
|
|
112
113
|
|
|
113
114
|
logdetective ./data/logs.txt
|
|
114
115
|
|
|
115
|
-
Examples of using different models. Note the use of `--
|
|
116
|
+
Examples of using different models. Note the use of `--filename-suffix` (or `-F`) option, useful for models that were quantized:
|
|
116
117
|
|
|
117
|
-
logdetective https://example.com/logs.txt --model QuantFactory/Meta-Llama-3-8B-Instruct-GGUF --
|
|
118
|
+
logdetective https://example.com/logs.txt --model QuantFactory/Meta-Llama-3-8B-Instruct-GGUF --filename-suffix Q5_K_S.gguf
|
|
118
119
|
logdetective https://kojipkgs.fedoraproject.org//work/tasks/3367/131313367/build.log --model 'fedora-copr/granite-3.2-8b-instruct-GGUF' -F Q4_K_M.gguf
|
|
119
120
|
|
|
120
121
|
Example of altered prompts:
|
|
@@ -126,7 +127,7 @@ Example of altered prompts:
|
|
|
126
127
|
|
|
127
128
|
Note that streaming with some models (notably Meta-Llama-3) is broken and can be worked around by `no-stream` option:
|
|
128
129
|
|
|
129
|
-
logdetective https://example.com/logs.txt --model QuantFactory/Meta-Llama-3-8B-Instruct-GGUF --
|
|
130
|
+
logdetective https://example.com/logs.txt --model QuantFactory/Meta-Llama-3-8B-Instruct-GGUF --filename-suffix Q5_K_M.gguf --no-stream
|
|
130
131
|
|
|
131
132
|
Choice of LLM
|
|
132
133
|
-------------
|
|
@@ -2,7 +2,7 @@ logdetective/__init__.py,sha256=VqRngDcuFT7JWms8Qc_MsOvajoXVOKPr-S1kqY3Pqhc,59
|
|
|
2
2
|
logdetective/constants.py,sha256=aCwrkBrDdS_kbNESK-Z-ewg--DSzodV2OMgwEq3UE38,2456
|
|
3
3
|
logdetective/drain3.ini,sha256=ni91eCT1TwTznZwcqWoOVMQcGEnWhEDNCoTPF7cfGfY,1360
|
|
4
4
|
logdetective/extractors.py,sha256=vT-je4NkDgSj9rRtSeLpqBU52gIUnnVgJPHFbVihpCw,5993
|
|
5
|
-
logdetective/logdetective.py,sha256=
|
|
5
|
+
logdetective/logdetective.py,sha256=W4yY5PDK0zO_6ObCnLQc6K6xY8zOd8MXJJDaE3LH6Wo,6224
|
|
6
6
|
logdetective/models.py,sha256=uczmQtWFgSp_ZGssngdTM4qzPF1o64dCy0469GoSbjQ,2937
|
|
7
7
|
logdetective/prompts-summary-first.yml,sha256=kmyMFQmqFXpojkz7p3CyCWCPxMpFLpfDdMGisB4YwL0,808
|
|
8
8
|
logdetective/prompts-summary-only.yml,sha256=8U9AMJV8ePW-0CoXOXlQoO92DAJDeutIT8ntSkkm6W0,470
|
|
@@ -12,29 +12,28 @@ logdetective/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
|
12
12
|
logdetective/server/compressors.py,sha256=y4aFYJ_9CbYdKuAI39Kc9GQSdPN8cSJ2c_VAz3T47EE,5249
|
|
13
13
|
logdetective/server/config.py,sha256=dYoqvexnMo8LBXhXezMIEqUwzTsRD-eWvRIFIYNv388,2540
|
|
14
14
|
logdetective/server/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
logdetective/server/database/base.py,sha256=
|
|
15
|
+
logdetective/server/database/base.py,sha256=bqMkhL2D96i_QiSnO5u1FqxYuJJu0m0wXLkqj_A9WBs,2093
|
|
16
16
|
logdetective/server/database/models/__init__.py,sha256=zoZMCt1_7tewDa6eEIIX_xrdN-tLegSiPNg5NiYaV3o,850
|
|
17
17
|
logdetective/server/database/models/exceptions.py,sha256=4ED7FSSA1liV9-7VIN2BwUiz6XlmP97Y1loKnsoNdD8,507
|
|
18
18
|
logdetective/server/database/models/koji.py,sha256=HNWxHYDxf4JN9K2ue8-V8dH-0XY5ZmxqH7Y9lAIbILA,6436
|
|
19
19
|
logdetective/server/database/models/merge_request_jobs.py,sha256=MxiAVKQIsQMbFylBsmYBmVXYvid-4_5mwwXLfWdp6_w,19965
|
|
20
|
-
logdetective/server/database/models/metrics.py,sha256=
|
|
20
|
+
logdetective/server/database/models/metrics.py,sha256=XpiGrZJ-SuHfePBOeek_WiV-i0p1wjoCBTekSMiZZM0,15559
|
|
21
21
|
logdetective/server/emoji.py,sha256=zSaYtLpSkpRCXpjMWnHR1bYwkmobMJASZ7YNalrd85U,5274
|
|
22
22
|
logdetective/server/exceptions.py,sha256=WN715KLL3ya6FiZ95v70VSbNuVhGuHFzxm2OeEPWQCw,981
|
|
23
23
|
logdetective/server/gitlab.py,sha256=X9JSotUUlG9bOWYbUNKt9KqLUAj6Uocd2KNpfn35ccU,17192
|
|
24
24
|
logdetective/server/koji.py,sha256=LG1pRiKUFvYFRKzgQoUG3pUHfcEwMoaMNjUSMKw_pBA,5640
|
|
25
25
|
logdetective/server/llm.py,sha256=wHMxRbAjI0q3osR5mRDR1kqww_6Pkc7JpF1mh9e6Mg8,10855
|
|
26
|
-
logdetective/server/metric.py,sha256=
|
|
27
|
-
logdetective/server/models.py,sha256=
|
|
28
|
-
logdetective/server/
|
|
29
|
-
logdetective/server/server.py,sha256=AM10P72tc_7N0GhH_N7msFhLr7ZGNgIfgTxt2sjasVE,30982
|
|
26
|
+
logdetective/server/metric.py,sha256=8ZhJNbl3eSzZiY0344YXMxLk_MkgjgZB6NcZsPozkkk,11317
|
|
27
|
+
logdetective/server/models.py,sha256=edAHzJoxMh-8v-JzSwHNS5FoV-v1PlmLI-3ZwxfBnf4,13303
|
|
28
|
+
logdetective/server/server.py,sha256=lCIctjXjkaOzto5H_qadYB6RLxAbbHvFOOwYdE_sIgY,29981
|
|
30
29
|
logdetective/server/templates/base_response.html.j2,sha256=BJGGV_Xb0Lnue8kq32oG9lI5CQDf9vce7HMYsP-Pvb4,2040
|
|
31
30
|
logdetective/server/templates/gitlab_full_comment.md.j2,sha256=4UujUzl3lmdbNEADsxn3HVrjfUiUu2FvUlp9MDFGXQI,2321
|
|
32
31
|
logdetective/server/templates/gitlab_short_comment.md.j2,sha256=2krnMlGqqju2V_6pE0UqUR1P674OFaeX5BMyY5htTOQ,2022
|
|
33
32
|
logdetective/server/utils.py,sha256=0BZ8WmzXNEtkUty1kOyFbBxDZWL0Icc8BUrxuHw9uvs,4015
|
|
34
33
|
logdetective/skip_snippets.yml,sha256=reGlhPPCo06nNUJWiC2LY-OJOoPdcyOB7QBTSMeh0eg,487
|
|
35
34
|
logdetective/utils.py,sha256=yalhySOF_Gzmqx_Ft9qad3TplAfZ6LOmauGXEJfKWiE,9803
|
|
36
|
-
logdetective-
|
|
37
|
-
logdetective-
|
|
38
|
-
logdetective-
|
|
39
|
-
logdetective-
|
|
40
|
-
logdetective-
|
|
35
|
+
logdetective-3.0.0.dist-info/METADATA,sha256=nHYpSkE4pz3W557RWhBUwQMh3i52KTlx7_rDXoL3wzQ,22874
|
|
36
|
+
logdetective-3.0.0.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
|
|
37
|
+
logdetective-3.0.0.dist-info/entry_points.txt,sha256=3K_vXja6PmcA8sNdUi63WdImeiNhVZcEGPTaoJmltfA,63
|
|
38
|
+
logdetective-3.0.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
39
|
+
logdetective-3.0.0.dist-info/RECORD,,
|
logdetective/server/plot.py
DELETED
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
from typing import Optional, Union, Dict
|
|
3
|
-
|
|
4
|
-
import numpy
|
|
5
|
-
from numpy.typing import ArrayLike
|
|
6
|
-
from matplotlib import dates, colormaps, axes, pyplot, figure
|
|
7
|
-
|
|
8
|
-
from logdetective.server.models import TimePeriod
|
|
9
|
-
from logdetective.server.database.models import (
|
|
10
|
-
AnalyzeRequestMetrics,
|
|
11
|
-
EndpointType,
|
|
12
|
-
Reactions,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Definition:
|
|
17
|
-
"""Define plot details, given a time period."""
|
|
18
|
-
|
|
19
|
-
def __init__(self, time_period: TimePeriod):
|
|
20
|
-
self.time_period = time_period
|
|
21
|
-
self.days_diff = time_period.get_time_period().days
|
|
22
|
-
if self.time_period.hours:
|
|
23
|
-
self._freq = "H"
|
|
24
|
-
self._time_format = "%Y-%m-%d %H"
|
|
25
|
-
self._locator = dates.HourLocator(interval=2)
|
|
26
|
-
self._time_unit = "hour"
|
|
27
|
-
self._time_delta = datetime.timedelta(hours=1)
|
|
28
|
-
elif self.time_period.days:
|
|
29
|
-
self._freq = "D"
|
|
30
|
-
self._time_format = "%Y-%m-%d"
|
|
31
|
-
self._locator = dates.DayLocator(interval=1)
|
|
32
|
-
self._time_unit = "day"
|
|
33
|
-
self._time_delta = datetime.timedelta(days=1)
|
|
34
|
-
elif self.time_period.weeks:
|
|
35
|
-
self._freq = "W"
|
|
36
|
-
self._time_format = "%Y-%m-%d"
|
|
37
|
-
self._locator = dates.WeekdayLocator(interval=1)
|
|
38
|
-
self._time_unit = "week"
|
|
39
|
-
self._time_delta = datetime.timedelta(weeks=1)
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def freq(self):
|
|
43
|
-
# pylint: disable=missing-function-docstring
|
|
44
|
-
return self._freq
|
|
45
|
-
|
|
46
|
-
@property
|
|
47
|
-
def time_format(self):
|
|
48
|
-
# pylint: disable=missing-function-docstring
|
|
49
|
-
return self._time_format
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def locator(self):
|
|
53
|
-
# pylint: disable=missing-function-docstring
|
|
54
|
-
return self._locator
|
|
55
|
-
|
|
56
|
-
@property
|
|
57
|
-
def time_unit(self):
|
|
58
|
-
# pylint: disable=missing-function-docstring
|
|
59
|
-
return self._time_unit
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def time_delta(self):
|
|
63
|
-
# pylint: disable=missing-function-docstring
|
|
64
|
-
return self._time_delta
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def create_time_series_arrays(
|
|
68
|
-
values_dict: dict[datetime.datetime, int],
|
|
69
|
-
plot_def: Definition,
|
|
70
|
-
start_time: datetime.datetime,
|
|
71
|
-
end_time: datetime.datetime,
|
|
72
|
-
value_type: Optional[Union[type[int], type[float]]] = int,
|
|
73
|
-
) -> tuple[numpy.ndarray, numpy.ndarray]:
|
|
74
|
-
"""Create time series arrays from a dictionary of values.
|
|
75
|
-
|
|
76
|
-
This function generates two aligned numpy arrays:
|
|
77
|
-
1. An array of timestamps from start_time to end_time
|
|
78
|
-
2. A corresponding array of valuesfor each timestamp
|
|
79
|
-
|
|
80
|
-
The timestamps are truncated to the precision specified by time_format.
|
|
81
|
-
If a timestamp in values_dict matches a generated timestamp, its values is used;
|
|
82
|
-
otherwise, the value defaults to zero.
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
values_dict: Dictionary mapping timestamps to their respective values
|
|
86
|
-
start_time: The starting timestamp of the time series
|
|
87
|
-
end_time: The ending timestamp of the time series
|
|
88
|
-
time_delta: The time interval between consecutive timestamps
|
|
89
|
-
time_format: String format for datetime truncation (e.g., '%Y-%m-%d %H:%M')
|
|
90
|
-
|
|
91
|
-
Returns:
|
|
92
|
-
A tuple containing:
|
|
93
|
-
- numpy.ndarray: Array of timestamps
|
|
94
|
-
- numpy.ndarray: Array of corresponding values
|
|
95
|
-
"""
|
|
96
|
-
num_intervals = int((end_time - start_time) / plot_def.time_delta) + 1
|
|
97
|
-
|
|
98
|
-
timestamps = numpy.array(
|
|
99
|
-
[
|
|
100
|
-
datetime.datetime.strptime(
|
|
101
|
-
(start_time + i * plot_def.time_delta).strftime(
|
|
102
|
-
format=plot_def.time_format
|
|
103
|
-
),
|
|
104
|
-
plot_def.time_format,
|
|
105
|
-
)
|
|
106
|
-
for i in range(num_intervals)
|
|
107
|
-
]
|
|
108
|
-
)
|
|
109
|
-
values = numpy.zeros(num_intervals, dtype=value_type)
|
|
110
|
-
|
|
111
|
-
timestamp_to_index = {timestamp: i for i, timestamp in enumerate(timestamps)}
|
|
112
|
-
|
|
113
|
-
for timestamp, count in values_dict.items():
|
|
114
|
-
if timestamp in timestamp_to_index:
|
|
115
|
-
values[timestamp_to_index[timestamp]] = count
|
|
116
|
-
|
|
117
|
-
return timestamps, values
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _add_bar_chart(
|
|
121
|
-
ax: axes.Axes,
|
|
122
|
-
plot_def: Definition,
|
|
123
|
-
timestamps: ArrayLike,
|
|
124
|
-
values: ArrayLike,
|
|
125
|
-
label: str,
|
|
126
|
-
) -> None:
|
|
127
|
-
"""Add a blue bar chart"""
|
|
128
|
-
bar_width = (
|
|
129
|
-
0.8 * plot_def.time_delta.total_seconds() / 86400
|
|
130
|
-
) # Convert to days for matplotlib
|
|
131
|
-
ax.bar(
|
|
132
|
-
timestamps,
|
|
133
|
-
values,
|
|
134
|
-
width=bar_width,
|
|
135
|
-
alpha=0.7,
|
|
136
|
-
color="skyblue",
|
|
137
|
-
label=label,
|
|
138
|
-
)
|
|
139
|
-
ax.set_xlabel("Time")
|
|
140
|
-
ax.set_ylabel(label, color="blue")
|
|
141
|
-
ax.tick_params(axis="y", labelcolor="blue")
|
|
142
|
-
|
|
143
|
-
ax.xaxis.set_major_formatter(dates.DateFormatter(plot_def.time_format))
|
|
144
|
-
ax.xaxis.set_major_locator(plot_def.locator)
|
|
145
|
-
|
|
146
|
-
pyplot.xticks(rotation=45)
|
|
147
|
-
|
|
148
|
-
ax.grid(True, alpha=0.3)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _add_line_chart( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
|
|
152
|
-
ax: axes.Axes,
|
|
153
|
-
timestamps: ArrayLike,
|
|
154
|
-
values: ArrayLike,
|
|
155
|
-
label: str,
|
|
156
|
-
color: str = "red",
|
|
157
|
-
set_label: bool = True,
|
|
158
|
-
):
|
|
159
|
-
"""Add a red line chart"""
|
|
160
|
-
ax.plot(timestamps, values, color=color, linestyle="-", linewidth=2, label=label)
|
|
161
|
-
if set_label:
|
|
162
|
-
ax.set_ylabel(label, color=color)
|
|
163
|
-
ax.tick_params(axis="y", labelcolor=color)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
async def requests_per_time(
|
|
167
|
-
period_of_time: TimePeriod,
|
|
168
|
-
endpoint: EndpointType = EndpointType.ANALYZE,
|
|
169
|
-
end_time: Optional[datetime.datetime] = None,
|
|
170
|
-
) -> figure.Figure:
|
|
171
|
-
"""
|
|
172
|
-
Generate a visualization of request counts over a specified time period.
|
|
173
|
-
|
|
174
|
-
This function creates a dual-axis plot showing:
|
|
175
|
-
1. A bar chart of request counts per time interval
|
|
176
|
-
2. A line chart showing the cumulative request count
|
|
177
|
-
|
|
178
|
-
The time intervals are determined by the provided TimePeriod object, which defines
|
|
179
|
-
the granularity and formatting of the time axis.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
period_of_time: A TimePeriod object that defines the time period and interval
|
|
183
|
-
for the analysis (e.g., hourly, daily, weekly)
|
|
184
|
-
endpoint: One of the API endpoints
|
|
185
|
-
end_time: The end time for the analysis period. If None, defaults to the current
|
|
186
|
-
UTC time
|
|
187
|
-
|
|
188
|
-
Returns:
|
|
189
|
-
A matplotlib Figure object containing the generated visualization
|
|
190
|
-
"""
|
|
191
|
-
end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
|
|
192
|
-
start_time = period_of_time.get_period_start_time(end_time)
|
|
193
|
-
plot_def = Definition(period_of_time)
|
|
194
|
-
requests_counts = await AnalyzeRequestMetrics.get_requests_in_period(
|
|
195
|
-
start_time, end_time, plot_def.time_format, endpoint
|
|
196
|
-
)
|
|
197
|
-
timestamps, counts = create_time_series_arrays(
|
|
198
|
-
requests_counts, plot_def, start_time, end_time
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
fig, ax1 = pyplot.subplots(figsize=(12, 6))
|
|
202
|
-
_add_bar_chart(ax1, plot_def, timestamps, counts, "Requests")
|
|
203
|
-
|
|
204
|
-
ax2 = ax1.twinx()
|
|
205
|
-
_add_line_chart(ax2, timestamps, numpy.cumsum(counts), "Cumulative Requests")
|
|
206
|
-
|
|
207
|
-
pyplot.title(
|
|
208
|
-
f"Requests received for API {endpoint} ({start_time.strftime(plot_def.time_format)} "
|
|
209
|
-
f"to {end_time.strftime(plot_def.time_format)})"
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
lines1, labels1 = ax1.get_legend_handles_labels()
|
|
213
|
-
lines2, labels2 = ax2.get_legend_handles_labels()
|
|
214
|
-
ax1.legend(lines1 + lines2, labels1 + labels2, loc="center")
|
|
215
|
-
|
|
216
|
-
pyplot.tight_layout()
|
|
217
|
-
|
|
218
|
-
return fig
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
async def average_time_per_responses( # pylint: disable=too-many-locals
|
|
222
|
-
period_of_time: TimePeriod,
|
|
223
|
-
endpoint: EndpointType = EndpointType.ANALYZE,
|
|
224
|
-
end_time: Optional[datetime.datetime] = None,
|
|
225
|
-
) -> figure.Figure:
|
|
226
|
-
"""
|
|
227
|
-
Generate a visualization of average response time and length over a specified time period.
|
|
228
|
-
|
|
229
|
-
This function creates a dual-axis plot showing:
|
|
230
|
-
1. A bar chart of average response time per time interval
|
|
231
|
-
1. A line chart of average response length per time interval
|
|
232
|
-
|
|
233
|
-
The time intervals are determined by the provided TimePeriod object, which defines
|
|
234
|
-
the granularity and formatting of the time axis.
|
|
235
|
-
|
|
236
|
-
Args:
|
|
237
|
-
period_of_time: A TimePeriod object that defines the time period and interval
|
|
238
|
-
for the analysis (e.g., hourly, daily, weekly)
|
|
239
|
-
endpoint: One of the API endpoints
|
|
240
|
-
end_time: The end time for the analysis period. If None, defaults to the current
|
|
241
|
-
UTC time
|
|
242
|
-
|
|
243
|
-
Returns:
|
|
244
|
-
A matplotlib Figure object containing the generated visualization
|
|
245
|
-
"""
|
|
246
|
-
end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
|
|
247
|
-
start_time = period_of_time.get_period_start_time(end_time)
|
|
248
|
-
plot_def = Definition(period_of_time)
|
|
249
|
-
responses_average_time = (
|
|
250
|
-
await AnalyzeRequestMetrics.get_responses_average_time_in_period(
|
|
251
|
-
start_time, end_time, plot_def.time_format, endpoint
|
|
252
|
-
)
|
|
253
|
-
)
|
|
254
|
-
timestamps, average_time = create_time_series_arrays(
|
|
255
|
-
responses_average_time,
|
|
256
|
-
plot_def,
|
|
257
|
-
start_time,
|
|
258
|
-
end_time,
|
|
259
|
-
float,
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
fig, ax1 = pyplot.subplots(figsize=(12, 6))
|
|
263
|
-
_add_bar_chart(
|
|
264
|
-
ax1, plot_def, timestamps, average_time, "average response time (seconds)"
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
responses_average_length = (
|
|
268
|
-
await AnalyzeRequestMetrics.get_responses_average_length_in_period(
|
|
269
|
-
start_time, end_time, plot_def.time_format, endpoint
|
|
270
|
-
)
|
|
271
|
-
)
|
|
272
|
-
timestamps, average_length = create_time_series_arrays(
|
|
273
|
-
responses_average_length,
|
|
274
|
-
plot_def,
|
|
275
|
-
start_time,
|
|
276
|
-
end_time,
|
|
277
|
-
float,
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
ax2 = ax1.twinx()
|
|
281
|
-
_add_line_chart(ax2, timestamps, average_length, "average response length (chars)")
|
|
282
|
-
|
|
283
|
-
pyplot.title(
|
|
284
|
-
f"average response time for API {endpoint} ({start_time.strftime(plot_def.time_format)} "
|
|
285
|
-
f"to {end_time.strftime(plot_def.time_format)})"
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
lines1, labels1 = ax1.get_legend_handles_labels()
|
|
289
|
-
lines2, labels2 = ax2.get_legend_handles_labels()
|
|
290
|
-
ax1.legend(lines1 + lines2, labels1 + labels2, loc="center")
|
|
291
|
-
|
|
292
|
-
pyplot.tight_layout()
|
|
293
|
-
|
|
294
|
-
return fig
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
async 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 = await 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: axes.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 = [
|
|
344
|
-
colormaps["viridis"](i)
|
|
345
|
-
for i in numpy.linspace(0, 1, len(reactions_values_dict))
|
|
346
|
-
]
|
|
347
|
-
|
|
348
|
-
first_emoji = True
|
|
349
|
-
for i, (emoji, dict_counts) in enumerate(reactions_values_dict.items()):
|
|
350
|
-
timestamps, counts = create_time_series_arrays(
|
|
351
|
-
dict_counts, plot_def, start_time, end_time
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
if first_emoji:
|
|
355
|
-
current_ax = ax
|
|
356
|
-
first_emoji = False
|
|
357
|
-
else:
|
|
358
|
-
current_ax = ax.twinx()
|
|
359
|
-
current_ax.spines["right"].set_position(("outward", 60 * (i - 1)))
|
|
360
|
-
|
|
361
|
-
_add_line_chart(current_ax, timestamps, counts, f"{emoji}", colors[i], False)
|
|
362
|
-
emoji_lines[emoji], emoji_labels[emoji] = current_ax.get_legend_handles_labels()
|
|
363
|
-
|
|
364
|
-
# Set the same y-limits for all axes
|
|
365
|
-
current_ax.set_ylim(0, max(all_counts) * 1.1)
|
|
366
|
-
|
|
367
|
-
# Only show y-ticks on the first axis to avoid clutter
|
|
368
|
-
if 0 < i < len(reactions_values_dict):
|
|
369
|
-
current_ax.set_yticks([])
|
|
370
|
-
|
|
371
|
-
return emoji_lines, emoji_labels
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
async def emojis_per_time(
|
|
375
|
-
period_of_time: TimePeriod,
|
|
376
|
-
end_time: Optional[datetime.datetime] = None,
|
|
377
|
-
) -> figure.Figure:
|
|
378
|
-
"""
|
|
379
|
-
Generate a visualization of overall emoji feedback
|
|
380
|
-
over a specified time period.
|
|
381
|
-
|
|
382
|
-
This function creates a multiple-axis plot showing
|
|
383
|
-
a line chart for every found emoji
|
|
384
|
-
|
|
385
|
-
The time intervals are determined by the provided TimePeriod object, which defines
|
|
386
|
-
the granularity and formatting of the time axis.
|
|
387
|
-
|
|
388
|
-
Args:
|
|
389
|
-
period_of_time: A TimePeriod object that defines the time period and interval
|
|
390
|
-
for the analysis (e.g., hourly, daily, weekly)
|
|
391
|
-
end_time: The end time for the analysis period. If None, defaults to the current
|
|
392
|
-
UTC time
|
|
393
|
-
|
|
394
|
-
Returns:
|
|
395
|
-
A matplotlib Figure object containing the generated visualization
|
|
396
|
-
"""
|
|
397
|
-
plot_def = Definition(period_of_time)
|
|
398
|
-
end_time = end_time or datetime.datetime.now(datetime.timezone.utc)
|
|
399
|
-
start_time = period_of_time.get_period_start_time(end_time)
|
|
400
|
-
reactions_values_dict = await _collect_emoji_data(start_time, plot_def)
|
|
401
|
-
|
|
402
|
-
fig, ax = pyplot.subplots(figsize=(12, 6))
|
|
403
|
-
|
|
404
|
-
emoji_lines, emoji_labels = _plot_emoji_data(
|
|
405
|
-
ax, reactions_values_dict, plot_def, start_time, end_time
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
pyplot.title(
|
|
409
|
-
f"Emoji feedback ({start_time.strftime(plot_def.time_format)} "
|
|
410
|
-
f"to {end_time.strftime(plot_def.time_format)})"
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
all_lines = []
|
|
414
|
-
for lines in emoji_lines.values():
|
|
415
|
-
all_lines.extend(lines)
|
|
416
|
-
all_labels = []
|
|
417
|
-
for labels in emoji_labels.values():
|
|
418
|
-
all_labels.extend(labels)
|
|
419
|
-
|
|
420
|
-
ax.legend(all_lines, all_labels, loc="upper left")
|
|
421
|
-
ax.set_xlabel("Time")
|
|
422
|
-
ax.set_ylabel("Count")
|
|
423
|
-
|
|
424
|
-
# Format x-axis
|
|
425
|
-
ax.xaxis.set_major_formatter(dates.DateFormatter(plot_def.time_format))
|
|
426
|
-
ax.xaxis.set_major_locator(plot_def.locator)
|
|
427
|
-
ax.tick_params(axis="x", labelrotation=45)
|
|
428
|
-
ax.grid(True, alpha=0.3)
|
|
429
|
-
|
|
430
|
-
pyplot.tight_layout()
|
|
431
|
-
|
|
432
|
-
return fig
|
|
File without changes
|
|
File without changes
|