logdetective 0.2.13__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {logdetective-0.2.13 → logdetective-0.3.1}/PKG-INFO +30 -1
- {logdetective-0.2.13 → logdetective-0.3.1}/README.md +24 -0
- {logdetective-0.2.13 → logdetective-0.3.1}/logdetective/constants.py +2 -3
- {logdetective-0.2.13 → logdetective-0.3.1}/logdetective/extractors.py +24 -14
- {logdetective-0.2.13 → logdetective-0.3.1}/logdetective/logdetective.py +69 -31
- logdetective-0.3.1/logdetective/server/models.py +173 -0
- logdetective-0.3.1/logdetective/server/server.py +482 -0
- logdetective-0.3.1/logdetective/server/utils.py +44 -0
- {logdetective-0.2.13 → logdetective-0.3.1}/logdetective/utils.py +56 -25
- {logdetective-0.2.13 → logdetective-0.3.1}/pyproject.toml +8 -5
- logdetective-0.2.13/logdetective/server/models.py +0 -82
- logdetective-0.2.13/logdetective/server/server.py +0 -262
- logdetective-0.2.13/logdetective/server/utils.py +0 -15
- {logdetective-0.2.13 → logdetective-0.3.1}/LICENSE +0 -0
- {logdetective-0.2.13 → logdetective-0.3.1}/logdetective/__init__.py +0 -0
- {logdetective-0.2.13 → logdetective-0.3.1}/logdetective/drain3.ini +0 -0
- {logdetective-0.2.13 → logdetective-0.3.1}/logdetective/server/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: logdetective
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -18,10 +18,15 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Classifier: Topic :: Internet :: Log Analysis
|
|
19
19
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
20
|
Classifier: Topic :: Software Development :: Debuggers
|
|
21
|
+
Provides-Extra: server
|
|
21
22
|
Requires-Dist: drain3 (>=0.9.11,<0.10.0)
|
|
23
|
+
Requires-Dist: fastapi (>=0.115.8,<0.116.0) ; extra == "server"
|
|
22
24
|
Requires-Dist: huggingface-hub (>0.23.2)
|
|
23
25
|
Requires-Dist: llama-cpp-python (>0.2.56,!=0.2.86)
|
|
24
26
|
Requires-Dist: numpy (>=1.26.0)
|
|
27
|
+
Requires-Dist: pydantic (>=2.10.6,<3.0.0) ; extra == "server"
|
|
28
|
+
Requires-Dist: python-gitlab (>=5.6.0,<6.0.0)
|
|
29
|
+
Requires-Dist: pyyaml (>=6.0.1,<7.0.0) ; extra == "server"
|
|
25
30
|
Requires-Dist: requests (>0.2.31)
|
|
26
31
|
Project-URL: homepage, https://github.com/fedora-copr/logdetective
|
|
27
32
|
Project-URL: issues, https://github.com/fedora-copr/logdetective/issues
|
|
@@ -216,6 +221,30 @@ $ curl -L -o models/mistral-7b-instruct-v0.2.Q4_K_S.gguf https://huggingface.co/
|
|
|
216
221
|
```
|
|
217
222
|
|
|
218
223
|
|
|
224
|
+
Our production instance
|
|
225
|
+
-----------------------
|
|
226
|
+
|
|
227
|
+
Our FastAPI server and model inference server run through `podman-compose` on an
|
|
228
|
+
Amazon AWS intance. The VM is provisioned by an
|
|
229
|
+
[ansible playbook](https://pagure.io/fedora-infra/ansible/blob/main/f/roles/logdetective/tasks/main.yml).
|
|
230
|
+
|
|
231
|
+
You can control the server through:
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
cd /root/logdetective
|
|
235
|
+
podman-compose -f docker-compose-prod.yaml ...
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The `/root` directory contains valuable data. If moving to a new instance,
|
|
239
|
+
please backup the whole directory and transfer it to the new instance.
|
|
240
|
+
|
|
241
|
+
Fore some reason, we need to manually run this command after every reboot:
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
|
|
219
248
|
License
|
|
220
249
|
-------
|
|
221
250
|
|
|
@@ -187,6 +187,30 @@ $ curl -L -o models/mistral-7b-instruct-v0.2.Q4_K_S.gguf https://huggingface.co/
|
|
|
187
187
|
```
|
|
188
188
|
|
|
189
189
|
|
|
190
|
+
Our production instance
|
|
191
|
+
-----------------------
|
|
192
|
+
|
|
193
|
+
Our FastAPI server and model inference server run through `podman-compose` on an
|
|
194
|
+
Amazon AWS intance. The VM is provisioned by an
|
|
195
|
+
[ansible playbook](https://pagure.io/fedora-infra/ansible/blob/main/f/roles/logdetective/tasks/main.yml).
|
|
196
|
+
|
|
197
|
+
You can control the server through:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
cd /root/logdetective
|
|
201
|
+
podman-compose -f docker-compose-prod.yaml ...
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The `/root` directory contains valuable data. If moving to a new instance,
|
|
205
|
+
please backup the whole directory and transfer it to the new instance.
|
|
206
|
+
|
|
207
|
+
Fore some reason, we need to manually run this command after every reboot:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
|
|
190
214
|
License
|
|
191
215
|
-------
|
|
192
216
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
# pylint: disable=line-too-long
|
|
3
2
|
DEFAULT_ADVISOR = "fedora-copr/Mistral-7B-Instruct-v0.2-GGUF"
|
|
4
3
|
|
|
@@ -32,7 +31,7 @@ Answer:
|
|
|
32
31
|
"""
|
|
33
32
|
|
|
34
33
|
SNIPPET_PROMPT_TEMPLATE = """
|
|
35
|
-
Analyse following RPM build log snippet.
|
|
34
|
+
Analyse following RPM build log snippet. Describe contents accurately, without speculation or suggestions for resolution.
|
|
36
35
|
|
|
37
36
|
Snippet:
|
|
38
37
|
|
|
@@ -59,4 +58,4 @@ Analysis:
|
|
|
59
58
|
|
|
60
59
|
"""
|
|
61
60
|
|
|
62
|
-
SNIPPET_DELIMITER =
|
|
61
|
+
SNIPPET_DELIMITER = "================"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import logging
|
|
3
|
+
from typing import Tuple
|
|
3
4
|
|
|
4
5
|
import drain3
|
|
5
6
|
from drain3.template_miner_config import TemplateMinerConfig
|
|
@@ -15,13 +16,17 @@ class LLMExtractor:
|
|
|
15
16
|
"""
|
|
16
17
|
A class that extracts relevant information from logs using a language model.
|
|
17
18
|
"""
|
|
19
|
+
|
|
18
20
|
def __init__(self, model: Llama, n_lines: int = 2):
|
|
19
21
|
self.model = model
|
|
20
22
|
self.n_lines = n_lines
|
|
21
23
|
self.grammar = LlamaGrammar.from_string(
|
|
22
|
-
|
|
24
|
+
'root ::= ("Yes" | "No")', verbose=False
|
|
25
|
+
)
|
|
23
26
|
|
|
24
|
-
def __call__(
|
|
27
|
+
def __call__(
|
|
28
|
+
self, log: str, n_lines: int = 2, neighbors: bool = False
|
|
29
|
+
) -> list[str]:
|
|
25
30
|
chunks = self.rate_chunks(log)
|
|
26
31
|
out = self.create_extract(chunks, neighbors)
|
|
27
32
|
return out
|
|
@@ -35,7 +40,7 @@ class LLMExtractor:
|
|
|
35
40
|
log_lines = log.split("\n")
|
|
36
41
|
|
|
37
42
|
for i in range(0, len(log_lines), self.n_lines):
|
|
38
|
-
block =
|
|
43
|
+
block = "\n".join(log_lines[i: i + self.n_lines])
|
|
39
44
|
prompt = SUMMARIZE_PROMPT_TEMPLATE.format(log)
|
|
40
45
|
out = self.model(prompt, max_tokens=7, grammar=self.grammar)
|
|
41
46
|
out = f"{out['choices'][0]['text']}\n"
|
|
@@ -44,8 +49,7 @@ class LLMExtractor:
|
|
|
44
49
|
return results
|
|
45
50
|
|
|
46
51
|
def create_extract(self, chunks: list[tuple], neighbors: bool = False) -> list[str]:
|
|
47
|
-
"""Extract interesting chunks from the model processing.
|
|
48
|
-
"""
|
|
52
|
+
"""Extract interesting chunks from the model processing."""
|
|
49
53
|
interesting = []
|
|
50
54
|
summary = []
|
|
51
55
|
# pylint: disable=consider-using-enumerate
|
|
@@ -64,8 +68,8 @@ class LLMExtractor:
|
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
class DrainExtractor:
|
|
67
|
-
"""A class that extracts information from logs using a template miner algorithm.
|
|
68
|
-
|
|
71
|
+
"""A class that extracts information from logs using a template miner algorithm."""
|
|
72
|
+
|
|
69
73
|
def __init__(self, verbose: bool = False, context: bool = False, max_clusters=8):
|
|
70
74
|
config = TemplateMinerConfig()
|
|
71
75
|
config.load(f"{os.path.dirname(__file__)}/drain3.ini")
|
|
@@ -75,15 +79,21 @@ class DrainExtractor:
|
|
|
75
79
|
self.verbose = verbose
|
|
76
80
|
self.context = context
|
|
77
81
|
|
|
78
|
-
def __call__(self, log: str) -> list[str]:
|
|
82
|
+
def __call__(self, log: str) -> list[Tuple[int, str]]:
|
|
79
83
|
out = []
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
# First pass create clusters
|
|
85
|
+
for _, chunk in get_chunks(log):
|
|
86
|
+
processed_chunk = self.miner.add_log_message(chunk)
|
|
87
|
+
LOG.debug(processed_chunk)
|
|
88
|
+
# Sort found clusters by size, descending order
|
|
89
|
+
sorted_clusters = sorted(
|
|
90
|
+
self.miner.drain.clusters, key=lambda it: it.size, reverse=True
|
|
91
|
+
)
|
|
92
|
+
# Second pass, only matching lines with clusters,
|
|
93
|
+
# to recover original text
|
|
94
|
+
for chunk_start, chunk in get_chunks(log):
|
|
85
95
|
cluster = self.miner.match(chunk, "always")
|
|
86
96
|
if cluster in sorted_clusters:
|
|
87
|
-
out.append(chunk)
|
|
97
|
+
out.append((chunk_start, chunk))
|
|
88
98
|
sorted_clusters.remove(cluster)
|
|
89
99
|
return out
|
|
@@ -4,40 +4,71 @@ import sys
|
|
|
4
4
|
|
|
5
5
|
from logdetective.constants import DEFAULT_ADVISOR
|
|
6
6
|
from logdetective.utils import (
|
|
7
|
-
process_log,
|
|
7
|
+
process_log,
|
|
8
|
+
initialize_model,
|
|
9
|
+
retrieve_log_content,
|
|
10
|
+
format_snippets,
|
|
11
|
+
compute_certainty,
|
|
12
|
+
)
|
|
8
13
|
from logdetective.extractors import LLMExtractor, DrainExtractor
|
|
9
14
|
|
|
10
15
|
LOG = logging.getLogger("logdetective")
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
def setup_args():
|
|
14
|
-
"""
|
|
19
|
+
"""Setup argument parser and return arguments."""
|
|
15
20
|
parser = argparse.ArgumentParser("logdetective")
|
|
16
|
-
parser.add_argument(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"file",
|
|
23
|
+
type=str,
|
|
24
|
+
default="",
|
|
25
|
+
help="The URL or path to the log file to be analyzed.",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"-M",
|
|
29
|
+
"--model",
|
|
30
|
+
help="The path or Hugging Face name of the language model for analysis.",
|
|
31
|
+
type=str,
|
|
32
|
+
default=DEFAULT_ADVISOR,
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"-F",
|
|
36
|
+
"--filename_suffix",
|
|
37
|
+
help="Suffix of the model file name to be retrieved from Hugging Face.\
|
|
23
38
|
Makes sense only if the model is specified with Hugging Face name.",
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
parser.add_argument("-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
parser.add_argument(
|
|
36
|
-
|
|
39
|
+
default="Q4_K_S.gguf",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument("-n", "--no-stream", action="store_true")
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"-S",
|
|
44
|
+
"--summarizer",
|
|
45
|
+
type=str,
|
|
46
|
+
default="drain",
|
|
47
|
+
help="Choose between LLM and Drain template miner as the log summarizer.\
|
|
48
|
+
LLM must be specified as path to a model, URL or local file.",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"-N",
|
|
52
|
+
"--n_lines",
|
|
53
|
+
type=int,
|
|
54
|
+
default=8,
|
|
55
|
+
help="The number of lines per chunk for LLM analysis.\
|
|
56
|
+
This only makes sense when you are summarizing with LLM.",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"-C",
|
|
60
|
+
"--n_clusters",
|
|
61
|
+
type=int,
|
|
62
|
+
default=8,
|
|
63
|
+
help="Number of clusters for Drain to organize log chunks into.\
|
|
64
|
+
This only makes sense when you are summarizing with Drain",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument("-v", "--verbose", action="count", default=0)
|
|
67
|
+
parser.add_argument("-q", "--quiet", action="store_true")
|
|
37
68
|
return parser.parse_args()
|
|
38
69
|
|
|
39
70
|
|
|
40
|
-
def main():
|
|
71
|
+
def main(): # pylint: disable=too-many-statements
|
|
41
72
|
"""Main execution function."""
|
|
42
73
|
args = setup_args()
|
|
43
74
|
|
|
@@ -57,8 +88,9 @@ def main():
|
|
|
57
88
|
|
|
58
89
|
# Primary model initialization
|
|
59
90
|
try:
|
|
60
|
-
model = initialize_model(
|
|
61
|
-
|
|
91
|
+
model = initialize_model(
|
|
92
|
+
args.model, filename_suffix=args.filename_suffix, verbose=args.verbose > 2
|
|
93
|
+
)
|
|
62
94
|
except ValueError as e:
|
|
63
95
|
LOG.error(e)
|
|
64
96
|
LOG.error("You likely do not have enough memory to load the AI model")
|
|
@@ -66,7 +98,9 @@ def main():
|
|
|
66
98
|
|
|
67
99
|
# Log file summarizer selection and initialization
|
|
68
100
|
if args.summarizer == "drain":
|
|
69
|
-
extractor = DrainExtractor(
|
|
101
|
+
extractor = DrainExtractor(
|
|
102
|
+
args.verbose > 1, context=True, max_clusters=args.n_clusters
|
|
103
|
+
)
|
|
70
104
|
else:
|
|
71
105
|
summarizer_model = initialize_model(args.summarizer, verbose=args.verbose > 2)
|
|
72
106
|
extractor = LLMExtractor(summarizer_model, args.verbose > 1)
|
|
@@ -81,7 +115,7 @@ def main():
|
|
|
81
115
|
sys.exit(4)
|
|
82
116
|
log_summary = extractor(log)
|
|
83
117
|
|
|
84
|
-
ratio = len(log_summary) / len(log.split(
|
|
118
|
+
ratio = len(log_summary) / len(log.split("\n"))
|
|
85
119
|
|
|
86
120
|
LOG.info("Compression ratio: %s", ratio)
|
|
87
121
|
|
|
@@ -103,15 +137,19 @@ def main():
|
|
|
103
137
|
|
|
104
138
|
if args.no_stream:
|
|
105
139
|
print(response["choices"][0]["text"])
|
|
106
|
-
probs = [
|
|
140
|
+
probs = [
|
|
141
|
+
{"logprob": e} for e in response["choices"][0]["logprobs"]["token_logprobs"]
|
|
142
|
+
]
|
|
107
143
|
|
|
108
144
|
else:
|
|
109
145
|
# Stream the output
|
|
110
146
|
for chunk in response:
|
|
111
147
|
if isinstance(chunk["choices"][0]["logprobs"], dict):
|
|
112
|
-
probs.append(
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
probs.append(
|
|
149
|
+
{"logprob": chunk["choices"][0]["logprobs"]["token_logprobs"][0]}
|
|
150
|
+
)
|
|
151
|
+
delta = chunk["choices"][0]["text"]
|
|
152
|
+
print(delta, end="", flush=True)
|
|
115
153
|
certainty = compute_certainty(probs)
|
|
116
154
|
|
|
117
155
|
print(f"\nResponse certainty: {certainty:.2f}%\n")
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from logging import BASIC_FORMAT
|
|
2
|
+
from typing import List, Dict, Optional
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BuildLog(BaseModel):
|
|
7
|
+
"""Model of data submitted to API."""
|
|
8
|
+
|
|
9
|
+
url: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JobHook(BaseModel):
|
|
13
|
+
"""Model of Job Hook events sent from GitLab.
|
|
14
|
+
Full details of the specification are available at
|
|
15
|
+
https://docs.gitlab.com/user/project/integrations/webhook_events/#job-events
|
|
16
|
+
This model implements only the fields that we care about. The webhook
|
|
17
|
+
sends many more fields that we will ignore."""
|
|
18
|
+
|
|
19
|
+
# The unique job ID on this GitLab instance.
|
|
20
|
+
build_id: int
|
|
21
|
+
|
|
22
|
+
# The identifier of the job. We only care about 'build_rpm' and
|
|
23
|
+
# 'build_centos_stream_rpm' jobs.
|
|
24
|
+
build_name: str = Field(pattern=r"^build(_.*)?_rpm$")
|
|
25
|
+
|
|
26
|
+
# A string representing the job status. We only care about 'failed' jobs.
|
|
27
|
+
build_status: str = Field(pattern=r"^failed$")
|
|
28
|
+
|
|
29
|
+
# The kind of webhook message. We are only interested in 'build' messages
|
|
30
|
+
# which represents job tasks in a pipeline.
|
|
31
|
+
object_kind: str = Field(pattern=r"^build$")
|
|
32
|
+
|
|
33
|
+
# The unique ID of the enclosing pipeline on this GitLab instance.
|
|
34
|
+
pipeline_id: int
|
|
35
|
+
|
|
36
|
+
# The unique ID of the project triggering this event
|
|
37
|
+
project_id: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Response(BaseModel):
|
|
41
|
+
"""Model of data returned by Log Detective API
|
|
42
|
+
|
|
43
|
+
explanation: CreateCompletionResponse
|
|
44
|
+
https://llama-cpp-python.readthedocs.io/en/latest/api-reference/#llama_cpp.llama_types.CreateCompletionResponse
|
|
45
|
+
response_certainty: float
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
explanation: Dict
|
|
49
|
+
response_certainty: float
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class StagedResponse(Response):
|
|
53
|
+
"""Model of data returned by Log Detective API when called when staged response
|
|
54
|
+
is requested. Contains list of reponses to prompts for individual snippets.
|
|
55
|
+
|
|
56
|
+
explanation: CreateCompletionResponse
|
|
57
|
+
https://llama-cpp-python.readthedocs.io/en/latest/api-reference/#llama_cpp.llama_types.CreateCompletionResponse
|
|
58
|
+
response_certainty: float
|
|
59
|
+
snippets:
|
|
60
|
+
list of dictionaries {
|
|
61
|
+
'snippet' : '<original_text>,
|
|
62
|
+
'comment': CreateCompletionResponse,
|
|
63
|
+
'line_number': '<location_in_log>' }
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
snippets: List[Dict[str, str | Dict | int]]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class InferenceConfig(BaseModel):
|
|
70
|
+
"""Model for inference configuration of logdetective server."""
|
|
71
|
+
|
|
72
|
+
max_tokens: int = -1
|
|
73
|
+
log_probs: int = 1
|
|
74
|
+
|
|
75
|
+
def __init__(self, data: Optional[dict] = None):
|
|
76
|
+
super().__init__()
|
|
77
|
+
if data is None:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
self.max_tokens = data.get("max_tokens", -1)
|
|
81
|
+
self.log_probs = data.get("log_probs", 1)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ExtractorConfig(BaseModel):
|
|
85
|
+
"""Model for extractor configuration of logdetective server."""
|
|
86
|
+
|
|
87
|
+
context: bool = True
|
|
88
|
+
max_clusters: int = 8
|
|
89
|
+
verbose: bool = False
|
|
90
|
+
|
|
91
|
+
def __init__(self, data: Optional[dict] = None):
|
|
92
|
+
super().__init__()
|
|
93
|
+
if data is None:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
self.context = data.get("context", True)
|
|
97
|
+
self.max_clusters = data.get("max_clusters", 8)
|
|
98
|
+
self.verbose = data.get("verbose", False)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class GitLabConfig(BaseModel):
|
|
102
|
+
"""Model for GitLab configuration of logdetective server."""
|
|
103
|
+
|
|
104
|
+
url: str = None
|
|
105
|
+
api_url: str = None
|
|
106
|
+
api_token: str = None
|
|
107
|
+
|
|
108
|
+
# Maximum size of artifacts.zip in MiB. (default: 300 MiB)
|
|
109
|
+
max_artifact_size: int = 300
|
|
110
|
+
|
|
111
|
+
def __init__(self, data: Optional[dict] = None):
|
|
112
|
+
super().__init__()
|
|
113
|
+
if data is None:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
self.url = data.get("url", "https://gitlab.com")
|
|
117
|
+
self.api_url = f"{self.url}/api/v4"
|
|
118
|
+
self.api_token = data.get("api_token", None)
|
|
119
|
+
self.max_artifact_size = int(data.get("max_artifact_size")) * 1024 * 1024
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class LogConfig(BaseModel):
|
|
123
|
+
"""Logging configuration"""
|
|
124
|
+
|
|
125
|
+
name: str = "logdetective"
|
|
126
|
+
level: str | int = "INFO"
|
|
127
|
+
path: str | None = None
|
|
128
|
+
format: str = BASIC_FORMAT
|
|
129
|
+
|
|
130
|
+
def __init__(self, data: Optional[dict] = None):
|
|
131
|
+
super().__init__()
|
|
132
|
+
if data is None:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
self.name = data.get("name", "logdetective")
|
|
136
|
+
self.level = data.get("level", "INFO").upper()
|
|
137
|
+
self.path = data.get("path")
|
|
138
|
+
self.format = data.get("format", BASIC_FORMAT)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class GeneralConfig(BaseModel):
|
|
142
|
+
"""General config options for Log Detective"""
|
|
143
|
+
|
|
144
|
+
packages: List[str] = None
|
|
145
|
+
|
|
146
|
+
def __init__(self, data: Optional[dict] = None):
|
|
147
|
+
super().__init__()
|
|
148
|
+
if data is None:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
self.packages = data.get("packages", [])
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Config(BaseModel):
|
|
155
|
+
"""Model for configuration of logdetective server."""
|
|
156
|
+
|
|
157
|
+
log: LogConfig = LogConfig()
|
|
158
|
+
inference: InferenceConfig = InferenceConfig()
|
|
159
|
+
extractor: ExtractorConfig = ExtractorConfig()
|
|
160
|
+
gitlab: GitLabConfig = GitLabConfig()
|
|
161
|
+
general: GeneralConfig = GeneralConfig()
|
|
162
|
+
|
|
163
|
+
def __init__(self, data: Optional[dict] = None):
|
|
164
|
+
super().__init__()
|
|
165
|
+
|
|
166
|
+
if data is None:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
self.log = LogConfig(data.get("log"))
|
|
170
|
+
self.inference = InferenceConfig(data.get("inference"))
|
|
171
|
+
self.extractor = ExtractorConfig(data.get("extractor"))
|
|
172
|
+
self.gitlab = GitLabConfig(data.get("gitlab"))
|
|
173
|
+
self.general = GeneralConfig(data.get("general"))
|