logdetective 0.4.0__py3-none-any.whl → 2.11.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/constants.py +33 -12
- logdetective/extractors.py +137 -68
- logdetective/logdetective.py +102 -33
- logdetective/models.py +99 -0
- logdetective/prompts-summary-first.yml +20 -0
- logdetective/prompts-summary-only.yml +13 -0
- logdetective/prompts.yml +90 -0
- logdetective/remote_log.py +67 -0
- logdetective/server/compressors.py +186 -0
- logdetective/server/config.py +78 -0
- logdetective/server/database/base.py +34 -26
- logdetective/server/database/models/__init__.py +33 -0
- logdetective/server/database/models/exceptions.py +17 -0
- logdetective/server/database/models/koji.py +143 -0
- logdetective/server/database/models/merge_request_jobs.py +623 -0
- logdetective/server/database/models/metrics.py +427 -0
- logdetective/server/emoji.py +148 -0
- logdetective/server/exceptions.py +37 -0
- logdetective/server/gitlab.py +451 -0
- logdetective/server/koji.py +159 -0
- logdetective/server/llm.py +309 -0
- logdetective/server/metric.py +75 -30
- logdetective/server/models.py +426 -23
- logdetective/server/plot.py +432 -0
- logdetective/server/server.py +580 -468
- logdetective/server/templates/base_response.html.j2 +59 -0
- logdetective/server/templates/gitlab_full_comment.md.j2 +73 -0
- logdetective/server/templates/gitlab_short_comment.md.j2 +62 -0
- logdetective/server/utils.py +98 -32
- logdetective/skip_snippets.yml +12 -0
- logdetective/utils.py +187 -73
- logdetective-2.11.0.dist-info/METADATA +568 -0
- logdetective-2.11.0.dist-info/RECORD +40 -0
- {logdetective-0.4.0.dist-info → logdetective-2.11.0.dist-info}/WHEEL +1 -1
- logdetective/server/database/models.py +0 -88
- logdetective-0.4.0.dist-info/METADATA +0 -333
- logdetective-0.4.0.dist-info/RECORD +0 -19
- {logdetective-0.4.0.dist-info → logdetective-2.11.0.dist-info}/entry_points.txt +0 -0
- {logdetective-0.4.0.dist-info → logdetective-2.11.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import asyncio
|
|
3
|
+
import random
|
|
4
|
+
import time
|
|
5
|
+
from typing import List, Tuple, Dict
|
|
6
|
+
|
|
7
|
+
import backoff
|
|
8
|
+
from fastapi import HTTPException
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
|
|
11
|
+
import aiohttp
|
|
12
|
+
from openai import AsyncStream
|
|
13
|
+
from openai.types.chat import ChatCompletionChunk
|
|
14
|
+
|
|
15
|
+
from logdetective.utils import (
|
|
16
|
+
compute_certainty,
|
|
17
|
+
prompt_to_messages,
|
|
18
|
+
format_snippets,
|
|
19
|
+
mine_logs,
|
|
20
|
+
)
|
|
21
|
+
from logdetective.server.config import (
|
|
22
|
+
LOG,
|
|
23
|
+
SERVER_CONFIG,
|
|
24
|
+
PROMPT_CONFIG,
|
|
25
|
+
CLIENT,
|
|
26
|
+
)
|
|
27
|
+
from logdetective.server.models import (
|
|
28
|
+
AnalyzedSnippet,
|
|
29
|
+
InferenceConfig,
|
|
30
|
+
Explanation,
|
|
31
|
+
StagedResponse,
|
|
32
|
+
SnippetAnalysis,
|
|
33
|
+
RatedSnippetAnalysis,
|
|
34
|
+
Response,
|
|
35
|
+
)
|
|
36
|
+
from logdetective.server.utils import (
|
|
37
|
+
format_analyzed_snippets,
|
|
38
|
+
should_we_giveup,
|
|
39
|
+
we_give_up,
|
|
40
|
+
filter_snippets,
|
|
41
|
+
construct_final_prompt,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
LLM_CPP_SERVER_TIMEOUT = os.environ.get("LLAMA_CPP_SERVER_TIMEOUT", 600)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@backoff.on_exception(
|
|
49
|
+
lambda: backoff.constant([10, 30, 120]),
|
|
50
|
+
aiohttp.ClientResponseError,
|
|
51
|
+
max_tries=4, # 4 tries and 3 retries
|
|
52
|
+
jitter=lambda wait_gen_value: random.uniform(wait_gen_value, wait_gen_value + 30),
|
|
53
|
+
giveup=should_we_giveup,
|
|
54
|
+
raise_on_giveup=False,
|
|
55
|
+
on_giveup=we_give_up,
|
|
56
|
+
)
|
|
57
|
+
async def call_llm(
|
|
58
|
+
messages: List[Dict[str, str]],
|
|
59
|
+
inference_cfg: InferenceConfig,
|
|
60
|
+
stream: bool = False,
|
|
61
|
+
structured_output: dict | None = None,
|
|
62
|
+
) -> Explanation:
|
|
63
|
+
"""Submit prompt to LLM.
|
|
64
|
+
inference_cfg: The configuration section from the config.json representing
|
|
65
|
+
the relevant inference server for this request.
|
|
66
|
+
"""
|
|
67
|
+
LOG.info("Analyzing the text")
|
|
68
|
+
|
|
69
|
+
LOG.info("Submitting to /v1/chat/completions endpoint")
|
|
70
|
+
|
|
71
|
+
kwargs = {}
|
|
72
|
+
|
|
73
|
+
# OpenAI API does not guarantee that the behavior for parameter set to `None`
|
|
74
|
+
# and parameter not given at all is the same.
|
|
75
|
+
# We build a dictionary of parameters based on the configuration.
|
|
76
|
+
if inference_cfg.log_probs:
|
|
77
|
+
LOG.info("Requesting log probabilities from LLM")
|
|
78
|
+
kwargs["logprobs"] = inference_cfg.log_probs
|
|
79
|
+
if structured_output:
|
|
80
|
+
LOG.info("Requesting structured output from LLM")
|
|
81
|
+
response_format = {
|
|
82
|
+
"type": "json_schema",
|
|
83
|
+
"json_schema": {
|
|
84
|
+
"name": "rated-snippet-analysis",
|
|
85
|
+
"schema": structured_output,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
kwargs["response_format"] = response_format
|
|
89
|
+
|
|
90
|
+
async with inference_cfg.get_limiter():
|
|
91
|
+
response = await CLIENT.chat.completions.create(
|
|
92
|
+
messages=messages,
|
|
93
|
+
max_tokens=inference_cfg.max_tokens,
|
|
94
|
+
stream=stream,
|
|
95
|
+
model=inference_cfg.model,
|
|
96
|
+
temperature=inference_cfg.temperature,
|
|
97
|
+
**kwargs,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not response.choices[0].message.content:
|
|
101
|
+
LOG.error("No response content recieved from %s", inference_cfg.url)
|
|
102
|
+
raise RuntimeError()
|
|
103
|
+
|
|
104
|
+
message_content = response.choices[0].message.content
|
|
105
|
+
|
|
106
|
+
if response.choices[0].logprobs and response.choices[0].logprobs.content:
|
|
107
|
+
logprobs = [e.to_dict() for e in response.choices[0].logprobs.content]
|
|
108
|
+
else:
|
|
109
|
+
logprobs = None
|
|
110
|
+
|
|
111
|
+
return Explanation(
|
|
112
|
+
text=message_content,
|
|
113
|
+
logprobs=logprobs,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@backoff.on_exception(
|
|
118
|
+
lambda: backoff.constant([10, 30, 120]),
|
|
119
|
+
aiohttp.ClientResponseError,
|
|
120
|
+
max_tries=4, # 4 tries and 3 retries
|
|
121
|
+
jitter=lambda wait_gen_value: random.uniform(wait_gen_value, wait_gen_value + 30),
|
|
122
|
+
giveup=should_we_giveup,
|
|
123
|
+
raise_on_giveup=False,
|
|
124
|
+
on_giveup=we_give_up,
|
|
125
|
+
)
|
|
126
|
+
async def call_llm_stream(
|
|
127
|
+
messages: List[Dict[str, str]],
|
|
128
|
+
inference_cfg: InferenceConfig,
|
|
129
|
+
stream: bool = False,
|
|
130
|
+
) -> AsyncStream[ChatCompletionChunk]:
|
|
131
|
+
"""Submit prompt to LLM and recieve stream of tokens as a result.
|
|
132
|
+
inference_cfg: The configuration section from the config.json representing
|
|
133
|
+
the relevant inference server for this request.
|
|
134
|
+
"""
|
|
135
|
+
LOG.info("Analyzing the text")
|
|
136
|
+
|
|
137
|
+
LOG.info("Submitting to /v1/chat/completions endpoint")
|
|
138
|
+
|
|
139
|
+
async with inference_cfg.get_limiter():
|
|
140
|
+
response = await CLIENT.chat.completions.create(
|
|
141
|
+
messages=messages,
|
|
142
|
+
max_tokens=inference_cfg.max_tokens,
|
|
143
|
+
logprobs=inference_cfg.log_probs,
|
|
144
|
+
stream=stream,
|
|
145
|
+
model=inference_cfg.model,
|
|
146
|
+
temperature=inference_cfg.temperature,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return response
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def analyze_snippets(
|
|
153
|
+
log_summary: List[Tuple[int, str]], structured_output: dict | None = None
|
|
154
|
+
) -> List[SnippetAnalysis | RatedSnippetAnalysis]:
|
|
155
|
+
"""Submit log file snippets to the LLM and gather results"""
|
|
156
|
+
# Process snippets asynchronously
|
|
157
|
+
awaitables = [
|
|
158
|
+
call_llm(
|
|
159
|
+
prompt_to_messages(
|
|
160
|
+
PROMPT_CONFIG.snippet_prompt_template.format(s),
|
|
161
|
+
PROMPT_CONFIG.snippet_system_prompt,
|
|
162
|
+
SERVER_CONFIG.inference.system_role,
|
|
163
|
+
SERVER_CONFIG.inference.user_role,
|
|
164
|
+
),
|
|
165
|
+
inference_cfg=SERVER_CONFIG.snippet_inference,
|
|
166
|
+
structured_output=structured_output,
|
|
167
|
+
)
|
|
168
|
+
for s in log_summary
|
|
169
|
+
]
|
|
170
|
+
gathered_responses = await asyncio.gather(*awaitables)
|
|
171
|
+
analyzed_snippets = []
|
|
172
|
+
|
|
173
|
+
for response in gathered_responses:
|
|
174
|
+
if structured_output:
|
|
175
|
+
try:
|
|
176
|
+
snippet = RatedSnippetAnalysis.model_validate_json(response.text)
|
|
177
|
+
except ValidationError as ex:
|
|
178
|
+
LOG.error("Invalid data structure returned `%s`", response.text)
|
|
179
|
+
raise ex
|
|
180
|
+
else:
|
|
181
|
+
snippet = SnippetAnalysis(text=response.text)
|
|
182
|
+
analyzed_snippets.append(snippet)
|
|
183
|
+
|
|
184
|
+
return analyzed_snippets
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def perfrom_analysis(log_text: str) -> Response:
|
|
188
|
+
"""Sumbit log file snippets in aggregate to LLM and retrieve results"""
|
|
189
|
+
log_summary = mine_logs(log_text, SERVER_CONFIG.extractor.get_extractors())
|
|
190
|
+
log_summary = format_snippets(log_summary)
|
|
191
|
+
|
|
192
|
+
final_prompt = construct_final_prompt(log_summary, PROMPT_CONFIG.prompt_template)
|
|
193
|
+
|
|
194
|
+
messages = prompt_to_messages(
|
|
195
|
+
final_prompt,
|
|
196
|
+
PROMPT_CONFIG.default_system_prompt,
|
|
197
|
+
SERVER_CONFIG.inference.system_role,
|
|
198
|
+
SERVER_CONFIG.inference.user_role,
|
|
199
|
+
)
|
|
200
|
+
response = await call_llm(
|
|
201
|
+
messages,
|
|
202
|
+
inference_cfg=SERVER_CONFIG.inference,
|
|
203
|
+
)
|
|
204
|
+
certainty = 0
|
|
205
|
+
|
|
206
|
+
if response.logprobs is not None:
|
|
207
|
+
try:
|
|
208
|
+
certainty = compute_certainty(response.logprobs)
|
|
209
|
+
except ValueError as ex:
|
|
210
|
+
LOG.error("Error encountered while computing certainty: %s", ex)
|
|
211
|
+
raise HTTPException(
|
|
212
|
+
status_code=400,
|
|
213
|
+
detail=f"Couldn't compute certainty with data:\n{response.logprobs}",
|
|
214
|
+
) from ex
|
|
215
|
+
|
|
216
|
+
return Response(explanation=response, response_certainty=certainty)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def perform_analyis_stream(log_text: str) -> AsyncStream:
|
|
220
|
+
"""Submit log file snippets in aggregate and return a stream of tokens"""
|
|
221
|
+
log_summary = mine_logs(log_text, SERVER_CONFIG.extractor.get_extractors())
|
|
222
|
+
log_summary = format_snippets(log_summary)
|
|
223
|
+
|
|
224
|
+
final_prompt = construct_final_prompt(log_summary, PROMPT_CONFIG.prompt_template)
|
|
225
|
+
|
|
226
|
+
messages = prompt_to_messages(
|
|
227
|
+
final_prompt,
|
|
228
|
+
PROMPT_CONFIG.default_system_prompt,
|
|
229
|
+
SERVER_CONFIG.inference.system_role,
|
|
230
|
+
SERVER_CONFIG.inference.user_role,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
stream = call_llm_stream(
|
|
234
|
+
messages,
|
|
235
|
+
inference_cfg=SERVER_CONFIG.inference,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# we need to figure out a better response here, this is how it looks rn:
|
|
239
|
+
# b'data: {"choices":[{"finish_reason":"stop","index":0,"delta":{}}],
|
|
240
|
+
# "created":1744818071,"id":"chatcmpl-c9geTxNcQO7M9wR...
|
|
241
|
+
return stream
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
async def perform_staged_analysis(log_text: str) -> StagedResponse:
|
|
245
|
+
"""Submit the log file snippets to the LLM and retrieve their results"""
|
|
246
|
+
log_summary = mine_logs(log_text, SERVER_CONFIG.extractor.get_extractors())
|
|
247
|
+
start = time.time()
|
|
248
|
+
if SERVER_CONFIG.general.top_k_snippets:
|
|
249
|
+
rated_snippets = await analyze_snippets(
|
|
250
|
+
log_summary=log_summary,
|
|
251
|
+
structured_output=RatedSnippetAnalysis.model_json_schema(),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Extract original text and line number from `log_summary`
|
|
255
|
+
processed_snippets = [
|
|
256
|
+
AnalyzedSnippet(line_number=e[0][0], text=e[0][1], explanation=e[1])
|
|
257
|
+
for e in zip(log_summary, rated_snippets)
|
|
258
|
+
]
|
|
259
|
+
processed_snippets = filter_snippets(
|
|
260
|
+
processed_snippets=processed_snippets,
|
|
261
|
+
top_k=SERVER_CONFIG.general.top_k_snippets,
|
|
262
|
+
)
|
|
263
|
+
LOG.info(
|
|
264
|
+
"Keeping %d of original %d snippets",
|
|
265
|
+
len(processed_snippets),
|
|
266
|
+
len(rated_snippets),
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
processed_snippets = await analyze_snippets(log_summary=log_summary)
|
|
270
|
+
|
|
271
|
+
# Extract original text and line number from `log_summary`
|
|
272
|
+
processed_snippets = [
|
|
273
|
+
AnalyzedSnippet(line_number=e[0][0], text=e[0][1], explanation=e[1])
|
|
274
|
+
for e in zip(log_summary, processed_snippets)
|
|
275
|
+
]
|
|
276
|
+
delta = time.time() - start
|
|
277
|
+
LOG.info("Snippet analysis performed in %f s", delta)
|
|
278
|
+
log_summary = format_analyzed_snippets(processed_snippets)
|
|
279
|
+
final_prompt = construct_final_prompt(log_summary, PROMPT_CONFIG.prompt_template_staged)
|
|
280
|
+
|
|
281
|
+
messages = prompt_to_messages(
|
|
282
|
+
final_prompt,
|
|
283
|
+
PROMPT_CONFIG.staged_system_prompt,
|
|
284
|
+
SERVER_CONFIG.inference.system_role,
|
|
285
|
+
SERVER_CONFIG.inference.user_role,
|
|
286
|
+
)
|
|
287
|
+
final_analysis = await call_llm(
|
|
288
|
+
messages,
|
|
289
|
+
inference_cfg=SERVER_CONFIG.inference,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
certainty = 0
|
|
293
|
+
|
|
294
|
+
if final_analysis.logprobs:
|
|
295
|
+
try:
|
|
296
|
+
certainty = compute_certainty(final_analysis.logprobs)
|
|
297
|
+
except ValueError as ex:
|
|
298
|
+
LOG.error("Error encountered while computing certainty: %s", ex)
|
|
299
|
+
raise HTTPException(
|
|
300
|
+
status_code=400,
|
|
301
|
+
detail=f"Couldn't compute certainty with data:\n"
|
|
302
|
+
f"{final_analysis.logprobs}",
|
|
303
|
+
) from ex
|
|
304
|
+
|
|
305
|
+
return StagedResponse(
|
|
306
|
+
explanation=final_analysis,
|
|
307
|
+
snippets=processed_snippets,
|
|
308
|
+
response_certainty=certainty,
|
|
309
|
+
)
|
logdetective/server/metric.py
CHANGED
|
@@ -1,35 +1,54 @@
|
|
|
1
|
-
import datetime
|
|
2
1
|
import inspect
|
|
3
|
-
|
|
2
|
+
import datetime
|
|
3
|
+
|
|
4
|
+
from typing import Optional, Union
|
|
4
5
|
from functools import wraps
|
|
5
6
|
|
|
7
|
+
import aiohttp
|
|
8
|
+
|
|
6
9
|
from starlette.responses import StreamingResponse
|
|
7
|
-
from logdetective.server.database.models import EndpointType, AnalyzeRequestMetrics
|
|
8
10
|
from logdetective.server import models
|
|
11
|
+
from logdetective.remote_log import RemoteLog
|
|
12
|
+
from logdetective.server.config import LOG
|
|
13
|
+
from logdetective.server.compressors import LLMResponseCompressor, RemoteLogCompressor
|
|
14
|
+
from logdetective.server.database.models import EndpointType, AnalyzeRequestMetrics
|
|
15
|
+
from logdetective.server.exceptions import LogDetectiveMetricsError
|
|
9
16
|
|
|
10
17
|
|
|
11
|
-
def add_new_metrics(
|
|
12
|
-
api_name:
|
|
18
|
+
async def add_new_metrics(
|
|
19
|
+
api_name: EndpointType,
|
|
20
|
+
url: Optional[str] = None,
|
|
21
|
+
http_session: Optional[aiohttp.ClientSession] = None,
|
|
22
|
+
received_at: Optional[datetime.datetime] = None,
|
|
23
|
+
compressed_log_content: Optional[bytes] = None,
|
|
13
24
|
) -> int:
|
|
14
25
|
"""Add a new database entry for a received request.
|
|
15
26
|
|
|
16
27
|
This will store the time when this function is called,
|
|
17
28
|
the endpoint from where the request was received,
|
|
18
|
-
and the log for which analysis is requested.
|
|
29
|
+
and the log (in a zip format) for which analysis is requested.
|
|
19
30
|
"""
|
|
20
|
-
|
|
31
|
+
if not compressed_log_content:
|
|
32
|
+
if not (url and http_session):
|
|
33
|
+
raise LogDetectiveMetricsError(
|
|
34
|
+
f"""Remote log can not be retrieved without URL and http session.
|
|
35
|
+
URL: {url}, http session:{http_session}""")
|
|
36
|
+
remote_log = RemoteLog(url, http_session)
|
|
37
|
+
compressed_log_content = await RemoteLogCompressor(remote_log).zip_content()
|
|
38
|
+
|
|
39
|
+
return await AnalyzeRequestMetrics.create(
|
|
21
40
|
endpoint=EndpointType(api_name),
|
|
22
|
-
|
|
41
|
+
compressed_log=compressed_log_content,
|
|
23
42
|
request_received_at=received_at
|
|
24
43
|
if received_at
|
|
25
44
|
else datetime.datetime.now(datetime.timezone.utc),
|
|
26
45
|
)
|
|
27
46
|
|
|
28
47
|
|
|
29
|
-
def update_metrics(
|
|
48
|
+
async def update_metrics(
|
|
30
49
|
metrics_id: int,
|
|
31
50
|
response: Union[models.Response, models.StagedResponse, StreamingResponse],
|
|
32
|
-
sent_at: datetime.datetime = None,
|
|
51
|
+
sent_at: Optional[datetime.datetime] = None,
|
|
33
52
|
) -> None:
|
|
34
53
|
"""Update a database metric entry for a received request,
|
|
35
54
|
filling data for the given response.
|
|
@@ -37,46 +56,72 @@ def update_metrics(
|
|
|
37
56
|
This will add to the database entry the time when the response was sent,
|
|
38
57
|
the length of the created response and the certainty for it.
|
|
39
58
|
"""
|
|
59
|
+
try:
|
|
60
|
+
compressed_response = LLMResponseCompressor(response).zip_response()
|
|
61
|
+
except AttributeError as e:
|
|
62
|
+
compressed_response = None
|
|
63
|
+
LOG.warning(
|
|
64
|
+
"Given response can not be serialized "
|
|
65
|
+
"and saved in db (probably a StreamingResponse): %s.",
|
|
66
|
+
e,
|
|
67
|
+
)
|
|
68
|
+
|
|
40
69
|
response_sent_at = (
|
|
41
70
|
sent_at if sent_at else datetime.datetime.now(datetime.timezone.utc)
|
|
42
71
|
)
|
|
43
72
|
response_length = None
|
|
44
|
-
if hasattr(response, "explanation") and
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if "text" in choice
|
|
49
|
-
)
|
|
73
|
+
if hasattr(response, "explanation") and isinstance(
|
|
74
|
+
response.explanation, models.Explanation
|
|
75
|
+
):
|
|
76
|
+
response_length = len(response.explanation.text)
|
|
50
77
|
response_certainty = (
|
|
51
78
|
response.response_certainty if hasattr(response, "response_certainty") else None
|
|
52
79
|
)
|
|
53
|
-
AnalyzeRequestMetrics.update(
|
|
54
|
-
metrics_id,
|
|
80
|
+
await AnalyzeRequestMetrics.update(
|
|
81
|
+
id_=metrics_id,
|
|
82
|
+
response_sent_at=response_sent_at,
|
|
83
|
+
response_length=response_length,
|
|
84
|
+
response_certainty=response_certainty,
|
|
85
|
+
compressed_response=compressed_response,
|
|
55
86
|
)
|
|
56
87
|
|
|
57
88
|
|
|
58
|
-
def track_request():
|
|
89
|
+
def track_request(name=None):
|
|
59
90
|
"""
|
|
60
|
-
Decorator to track requests metrics
|
|
91
|
+
Decorator to track requests/responses metrics
|
|
92
|
+
|
|
93
|
+
On entering the decorated function, it registers the time for the request
|
|
94
|
+
and saves the passed log content.
|
|
95
|
+
On exiting the decorated function, it registers the time for the response
|
|
96
|
+
and saves the generated response.
|
|
97
|
+
|
|
98
|
+
Use it to decorate server endpoints that generate a llm response
|
|
99
|
+
as in the following example:
|
|
100
|
+
|
|
101
|
+
>>> @app.post("/analyze", response_model=Response)
|
|
102
|
+
>>> @track_request()
|
|
103
|
+
>>> async def analyze_log(build_log)
|
|
104
|
+
>>> pass
|
|
105
|
+
|
|
106
|
+
Warning: the decorators' order is important!
|
|
107
|
+
The function returned by the *track_request* decorator is the
|
|
108
|
+
server API function we want to be called by FastAPI.
|
|
61
109
|
"""
|
|
62
110
|
|
|
63
111
|
def decorator(f):
|
|
64
112
|
@wraps(f)
|
|
65
113
|
async def async_decorated_function(*args, **kwargs):
|
|
66
|
-
|
|
114
|
+
log_url = kwargs["build_log"].url
|
|
115
|
+
metrics_id = await add_new_metrics(
|
|
116
|
+
api_name=EndpointType(name if name else f.__name__),
|
|
117
|
+
url=log_url, http_session=kwargs["http_session"]
|
|
118
|
+
)
|
|
67
119
|
response = await f(*args, **kwargs)
|
|
68
|
-
update_metrics(metrics_id, response)
|
|
69
|
-
return response
|
|
70
|
-
|
|
71
|
-
@wraps(f)
|
|
72
|
-
def sync_decorated_function(*args, **kwargs):
|
|
73
|
-
metrics_id = add_new_metrics(f.__name__, kwargs["build_log"])
|
|
74
|
-
response = f(*args, **kwargs)
|
|
75
|
-
update_metrics(metrics_id, response)
|
|
120
|
+
await update_metrics(metrics_id, response)
|
|
76
121
|
return response
|
|
77
122
|
|
|
78
123
|
if inspect.iscoroutinefunction(f):
|
|
79
124
|
return async_decorated_function
|
|
80
|
-
|
|
125
|
+
raise NotImplementedError("An async coroutine is needed")
|
|
81
126
|
|
|
82
127
|
return decorator
|