rucio-clients 35.8.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.
- rucio/__init__.py +17 -0
- rucio/alembicrevision.py +15 -0
- rucio/client/__init__.py +15 -0
- rucio/client/accountclient.py +433 -0
- rucio/client/accountlimitclient.py +183 -0
- rucio/client/baseclient.py +974 -0
- rucio/client/client.py +76 -0
- rucio/client/configclient.py +126 -0
- rucio/client/credentialclient.py +59 -0
- rucio/client/didclient.py +866 -0
- rucio/client/diracclient.py +56 -0
- rucio/client/downloadclient.py +1785 -0
- rucio/client/exportclient.py +44 -0
- rucio/client/fileclient.py +50 -0
- rucio/client/importclient.py +42 -0
- rucio/client/lifetimeclient.py +90 -0
- rucio/client/lockclient.py +109 -0
- rucio/client/metaconventionsclient.py +140 -0
- rucio/client/pingclient.py +44 -0
- rucio/client/replicaclient.py +454 -0
- rucio/client/requestclient.py +125 -0
- rucio/client/rseclient.py +746 -0
- rucio/client/ruleclient.py +294 -0
- rucio/client/scopeclient.py +90 -0
- rucio/client/subscriptionclient.py +173 -0
- rucio/client/touchclient.py +82 -0
- rucio/client/uploadclient.py +955 -0
- rucio/common/__init__.py +13 -0
- rucio/common/cache.py +74 -0
- rucio/common/config.py +801 -0
- rucio/common/constants.py +159 -0
- rucio/common/constraints.py +17 -0
- rucio/common/didtype.py +189 -0
- rucio/common/exception.py +1151 -0
- rucio/common/extra.py +36 -0
- rucio/common/logging.py +420 -0
- rucio/common/pcache.py +1408 -0
- rucio/common/plugins.py +153 -0
- rucio/common/policy.py +84 -0
- rucio/common/schema/__init__.py +150 -0
- rucio/common/schema/atlas.py +413 -0
- rucio/common/schema/belleii.py +408 -0
- rucio/common/schema/domatpc.py +401 -0
- rucio/common/schema/escape.py +426 -0
- rucio/common/schema/generic.py +433 -0
- rucio/common/schema/generic_multi_vo.py +412 -0
- rucio/common/schema/icecube.py +406 -0
- rucio/common/stomp_utils.py +159 -0
- rucio/common/stopwatch.py +55 -0
- rucio/common/test_rucio_server.py +148 -0
- rucio/common/types.py +403 -0
- rucio/common/utils.py +2238 -0
- rucio/rse/__init__.py +96 -0
- rucio/rse/protocols/__init__.py +13 -0
- rucio/rse/protocols/bittorrent.py +184 -0
- rucio/rse/protocols/cache.py +122 -0
- rucio/rse/protocols/dummy.py +111 -0
- rucio/rse/protocols/gfal.py +703 -0
- rucio/rse/protocols/globus.py +243 -0
- rucio/rse/protocols/gsiftp.py +92 -0
- rucio/rse/protocols/http_cache.py +82 -0
- rucio/rse/protocols/mock.py +123 -0
- rucio/rse/protocols/ngarc.py +209 -0
- rucio/rse/protocols/posix.py +250 -0
- rucio/rse/protocols/protocol.py +594 -0
- rucio/rse/protocols/rclone.py +364 -0
- rucio/rse/protocols/rfio.py +136 -0
- rucio/rse/protocols/srm.py +338 -0
- rucio/rse/protocols/ssh.py +413 -0
- rucio/rse/protocols/storm.py +206 -0
- rucio/rse/protocols/webdav.py +550 -0
- rucio/rse/protocols/xrootd.py +301 -0
- rucio/rse/rsemanager.py +764 -0
- rucio/vcsversion.py +11 -0
- rucio/version.py +38 -0
- rucio_clients-35.8.2.data/data/etc/rse-accounts.cfg.template +25 -0
- rucio_clients-35.8.2.data/data/etc/rucio.cfg.atlas.client.template +42 -0
- rucio_clients-35.8.2.data/data/etc/rucio.cfg.template +257 -0
- rucio_clients-35.8.2.data/data/requirements.client.txt +15 -0
- rucio_clients-35.8.2.data/data/rucio_client/merge_rucio_configs.py +144 -0
- rucio_clients-35.8.2.data/scripts/rucio +2542 -0
- rucio_clients-35.8.2.data/scripts/rucio-admin +2447 -0
- rucio_clients-35.8.2.dist-info/METADATA +50 -0
- rucio_clients-35.8.2.dist-info/RECORD +88 -0
- rucio_clients-35.8.2.dist-info/WHEEL +5 -0
- rucio_clients-35.8.2.dist-info/licenses/AUTHORS.rst +97 -0
- rucio_clients-35.8.2.dist-info/licenses/LICENSE +201 -0
- rucio_clients-35.8.2.dist-info/top_level.txt +1 -0
rucio/common/extra.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import importlib
|
|
16
|
+
import warnings
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def import_extras(module_list: list[str]) -> "dict[str, Any]":
|
|
24
|
+
out = dict()
|
|
25
|
+
for mod in module_list:
|
|
26
|
+
out[mod] = None
|
|
27
|
+
try:
|
|
28
|
+
with warnings.catch_warnings():
|
|
29
|
+
# TODO: remove when https://github.com/paramiko/paramiko/issues/2038 is fixed
|
|
30
|
+
warnings.filterwarnings('ignore', 'Blowfish has been deprecated', module='paramiko')
|
|
31
|
+
# TODO: deprecated python 2 and 3.6 too ...
|
|
32
|
+
warnings.filterwarnings('ignore', 'Python .* is no longer supported', module='paramiko')
|
|
33
|
+
out[mod] = importlib.import_module(mod)
|
|
34
|
+
except ImportError:
|
|
35
|
+
pass
|
|
36
|
+
return out
|
rucio/common/logging.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# Copyright European Organization for Nuclear Research (CERN) since 2012
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import datetime
|
|
15
|
+
import functools
|
|
16
|
+
import itertools
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
22
|
+
from traceback import format_tb
|
|
23
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, get_args
|
|
24
|
+
|
|
25
|
+
from rucio.common.config import config_get, config_get_bool
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from logging import LogRecord, _SysExcInfoType
|
|
29
|
+
|
|
30
|
+
from _typeshed import OptExcInfo
|
|
31
|
+
from flask import Flask
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Mapping from ECS field paths
|
|
35
|
+
# https://www.elastic.co/guide/en/ecs-logging/overview/current/intro.html#_field_mapping
|
|
36
|
+
# https://www.elastic.co/guide/en/ecs/8.5/ecs-field-reference.html
|
|
37
|
+
# to python log record attributes:
|
|
38
|
+
# https://docs.python.org/3/library/logging.html#logrecord-attributes
|
|
39
|
+
ECS_FIELDS = Literal[
|
|
40
|
+
'@timestamp',
|
|
41
|
+
'message',
|
|
42
|
+
'log.level',
|
|
43
|
+
'log.origin.function',
|
|
44
|
+
'log.origin.file.line',
|
|
45
|
+
'log.origin.file.name',
|
|
46
|
+
'log.logger',
|
|
47
|
+
'process.pid',
|
|
48
|
+
'process.name',
|
|
49
|
+
'process.thread.id',
|
|
50
|
+
'process.thread.name'
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
LOG_RECORDS = Literal[
|
|
54
|
+
'asctime',
|
|
55
|
+
'message',
|
|
56
|
+
'levelname',
|
|
57
|
+
'funcName',
|
|
58
|
+
'lineno',
|
|
59
|
+
'filename',
|
|
60
|
+
'name',
|
|
61
|
+
'process',
|
|
62
|
+
'processName',
|
|
63
|
+
'thread',
|
|
64
|
+
'threadName'
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
BUILTIN_FIELDS: tuple[tuple[ECS_FIELDS, LOG_RECORDS], ...] = tuple((x, y) for x, y in zip(get_args(ECS_FIELDS), get_args(LOG_RECORDS)))
|
|
68
|
+
ECS_TO_LOG_RECORD_MAP: dict[ECS_FIELDS, LOG_RECORDS] = dict(BUILTIN_FIELDS)
|
|
69
|
+
LOG_RECORD_TO_ECS_MAP: dict[LOG_RECORDS, ECS_FIELDS] = dict((f[1], f[0]) for f in BUILTIN_FIELDS)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _json_serializable(obj: Any) -> Union[dict[Any, Any], str]:
|
|
73
|
+
try:
|
|
74
|
+
return obj.__dict__
|
|
75
|
+
except AttributeError:
|
|
76
|
+
return str(obj)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _navigate_path(obj: Any, path: Sequence[str]) -> Optional[Any]:
|
|
80
|
+
"""
|
|
81
|
+
Traverse the path in the given object either via attributes or via dict-like subscriptions.
|
|
82
|
+
Returns the found value; None if navigation fails
|
|
83
|
+
|
|
84
|
+
For example, for an input
|
|
85
|
+
obj = request # flask "request" object https://flask.palletsprojects.com/en/2.1.x/api/#flask.Request
|
|
86
|
+
path = ['headers', 'X-Rucio-Auth-Token']
|
|
87
|
+
returns the value found in
|
|
88
|
+
request.headers['X-Rucio-Auth-Token']
|
|
89
|
+
"""
|
|
90
|
+
value = obj
|
|
91
|
+
i = 0
|
|
92
|
+
while value and i < len(path):
|
|
93
|
+
p = path[i]
|
|
94
|
+
try:
|
|
95
|
+
value = getattr(value, p)
|
|
96
|
+
except AttributeError:
|
|
97
|
+
try:
|
|
98
|
+
# Allow integers for access into arrays
|
|
99
|
+
p = int(p)
|
|
100
|
+
except ValueError:
|
|
101
|
+
pass
|
|
102
|
+
try:
|
|
103
|
+
value = value[p]
|
|
104
|
+
except (TypeError, KeyError):
|
|
105
|
+
value = None
|
|
106
|
+
i += 1
|
|
107
|
+
if value is obj:
|
|
108
|
+
return None
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _unflatten_dict(dictionary: dict[str, Any]) -> dict[str, Any]:
|
|
113
|
+
"""
|
|
114
|
+
Transform a dict of the form
|
|
115
|
+
{'a.b.c': value1, 'a.b.d': value2, 'z': value3}
|
|
116
|
+
into
|
|
117
|
+
{'a': {'b': {'c': value1, 'd': value2}}, 'z': value3}
|
|
118
|
+
|
|
119
|
+
On incompatible input keys (for example: 'a.b.c', 'a', 'a.d'), the last key wins
|
|
120
|
+
"""
|
|
121
|
+
ret = {}
|
|
122
|
+
for k, v in dictionary.items():
|
|
123
|
+
path = k.split('.')
|
|
124
|
+
d = ret
|
|
125
|
+
i = 0
|
|
126
|
+
while i < len(path) - 1:
|
|
127
|
+
existing_v = d.get(path[i])
|
|
128
|
+
if isinstance(existing_v, dict):
|
|
129
|
+
d = existing_v
|
|
130
|
+
else:
|
|
131
|
+
d[path[i]] = {}
|
|
132
|
+
d = d[path[i]]
|
|
133
|
+
i += 1
|
|
134
|
+
if i == len(path) - 1:
|
|
135
|
+
d[path[i]] = v
|
|
136
|
+
return ret
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _get_request_data(request_path: Sequence[str]) -> "Callable[[LogDataSource, LogRecord], Iterator[tuple[str, Optional[Any]]]]":
|
|
140
|
+
"""
|
|
141
|
+
Returns a function which, when called, will resolve the value
|
|
142
|
+
in the flask request object at request_path
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
# The import fails if imported inside a client due to rsemanager.
|
|
146
|
+
# TODO: move to top of file once we got rid of/refactored rsemanager
|
|
147
|
+
from flask import has_request_context, request
|
|
148
|
+
|
|
149
|
+
def _request_data_formatter(record_formatter: "LogDataSource", record: "LogRecord") -> Iterator[tuple[str, Optional[Any]]]:
|
|
150
|
+
value = None
|
|
151
|
+
if has_request_context() and request_path:
|
|
152
|
+
value = _navigate_path(request, request_path)
|
|
153
|
+
yield record_formatter.ecs_fields[0], str(value) if value is not None else None
|
|
154
|
+
|
|
155
|
+
return _request_data_formatter
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_record_attribute(attribute: str) -> "Callable[[LogDataSource, LogRecord], Iterator[tuple[str, Optional[Any]]]]":
|
|
159
|
+
"""
|
|
160
|
+
Returns a function which, when called, will generate the value of the desired attribute from
|
|
161
|
+
the record passed in argument.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def _record_attribute_formatter(record_formatter: "LogDataSource", record: "LogRecord") -> Iterator[tuple[str, Optional[Any]]]:
|
|
165
|
+
value = None
|
|
166
|
+
try:
|
|
167
|
+
value = getattr(record, attribute)
|
|
168
|
+
except AttributeError:
|
|
169
|
+
pass
|
|
170
|
+
yield record_formatter.ecs_fields[0], value
|
|
171
|
+
|
|
172
|
+
return _record_attribute_formatter
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _timestamp_formatter(record_formatter: "LogDataSource", record: "LogRecord") -> Iterator[tuple[str, Optional[Any]]]:
|
|
176
|
+
"""
|
|
177
|
+
Format a timestamp
|
|
178
|
+
"""
|
|
179
|
+
yield record_formatter.ecs_fields[0], datetime.datetime.utcfromtimestamp(record.created).isoformat(timespec='milliseconds') + 'Z'
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _ecs_field_to_record_attribute(field_name: Union[ECS_FIELDS, str]) -> Union[LOG_RECORDS, str]:
|
|
183
|
+
"""
|
|
184
|
+
Sanitize the path-like field name into a symbol which can be the name of an object attribute.
|
|
185
|
+
"""
|
|
186
|
+
record = ECS_TO_LOG_RECORD_MAP.get(field_name) # type: ignore
|
|
187
|
+
if record:
|
|
188
|
+
return record
|
|
189
|
+
return field_name.replace('-', '_').replace('.', '_')
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class LogDataSource:
|
|
193
|
+
"""
|
|
194
|
+
Represents one log data source and allows to format it into one or more json fields
|
|
195
|
+
"""
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
ecs_fields: tuple[str, ...],
|
|
199
|
+
formatter: "Optional[Callable[[LogDataSource, LogRecord], Iterator[tuple[str, Optional[Any]]]]]" = None,
|
|
200
|
+
dst_record_attr: Optional[str] = None
|
|
201
|
+
):
|
|
202
|
+
self.ecs_fields = ecs_fields
|
|
203
|
+
self._formatter = formatter
|
|
204
|
+
self._dst_record_attr = dst_record_attr
|
|
205
|
+
|
|
206
|
+
def __hash__(self):
|
|
207
|
+
return hash(self.ecs_fields)
|
|
208
|
+
|
|
209
|
+
def __eq__(self, other: Any):
|
|
210
|
+
if not other or not isinstance(other, self.__class__):
|
|
211
|
+
return False
|
|
212
|
+
return self.ecs_fields == other.ecs_fields
|
|
213
|
+
|
|
214
|
+
def __str__(self):
|
|
215
|
+
return self.__class__.__name__ + '(' + ', '.join(self.ecs_fields) + ')'
|
|
216
|
+
|
|
217
|
+
def format(self, record: "LogRecord") -> Optional[Iterator[tuple[str, Any]]]:
|
|
218
|
+
if not self._formatter:
|
|
219
|
+
return
|
|
220
|
+
for field_name, field_value in self._formatter(self, record):
|
|
221
|
+
if self._dst_record_attr:
|
|
222
|
+
setattr(record, self._dst_record_attr, field_value)
|
|
223
|
+
yield field_name, field_value
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class MessageLogDataSource(LogDataSource):
|
|
227
|
+
def __init__(self):
|
|
228
|
+
super().__init__(
|
|
229
|
+
ecs_fields=('message', 'error.type', 'error.message', 'error.stack_trace'),
|
|
230
|
+
formatter=None,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def _get_exc_info(record: "LogRecord") -> Optional[Union["OptExcInfo", "_SysExcInfoType"]]:
|
|
235
|
+
exc_info = record.exc_info
|
|
236
|
+
if not exc_info:
|
|
237
|
+
return None
|
|
238
|
+
if isinstance(exc_info, bool):
|
|
239
|
+
exc_info = sys.exc_info()
|
|
240
|
+
if isinstance(exc_info, (list, tuple)):
|
|
241
|
+
return exc_info
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
def format(self, record: "LogRecord") -> Iterator[tuple[str, Optional[str]]]:
|
|
245
|
+
exc_info = self._get_exc_info(record)
|
|
246
|
+
message = record.getMessage()
|
|
247
|
+
error_type, error_message, stack_trace = None, None, None
|
|
248
|
+
if exc_info:
|
|
249
|
+
error_type = exc_info[0].__name__ if exc_info[0] else None
|
|
250
|
+
error_message = str(exc_info[1]) if exc_info[1] else None
|
|
251
|
+
stack_trace = "".join(format_tb(record.exc_info[2])) or None if exc_info[2] else None
|
|
252
|
+
if not stack_trace:
|
|
253
|
+
stack_trace = str(getattr(record, "stack_info", '')) or None
|
|
254
|
+
|
|
255
|
+
# Set the message into the record field
|
|
256
|
+
s = message
|
|
257
|
+
if error_message:
|
|
258
|
+
if s[-1:] != "\n":
|
|
259
|
+
s = s + "\n"
|
|
260
|
+
s = s + error_message
|
|
261
|
+
if stack_trace:
|
|
262
|
+
if s[-1:] != "\n":
|
|
263
|
+
s = s + "\n"
|
|
264
|
+
s = s + stack_trace
|
|
265
|
+
record.message = s
|
|
266
|
+
|
|
267
|
+
yield from zip(self.ecs_fields, (record.message, error_type, error_message, stack_trace))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class ConstantStrDataSource(LogDataSource):
|
|
271
|
+
"""
|
|
272
|
+
Prints a constant string for the given ECS field.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def __init__(self, ecs_field: ECS_FIELDS, _str: str):
|
|
276
|
+
log_record = ECS_TO_LOG_RECORD_MAP.get(ecs_field, None)
|
|
277
|
+
self._str = _str
|
|
278
|
+
|
|
279
|
+
def _formatter(data_source: LogDataSource, record: "LogRecord") -> Iterator[tuple[str, str]]:
|
|
280
|
+
yield self.ecs_fields[0], self._str
|
|
281
|
+
|
|
282
|
+
super().__init__(ecs_fields=(ecs_field,), formatter=_formatter, dst_record_attr=log_record)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class RucioFormatter(logging.Formatter):
|
|
286
|
+
"""
|
|
287
|
+
The formatter should be a drop-in replacement to the python builtin
|
|
288
|
+
formatter, with two additional additions:
|
|
289
|
+
- it can output directly to json
|
|
290
|
+
- it can include data from the flask 'request' object into the format
|
|
291
|
+
|
|
292
|
+
When the logger writes to a json format, it tries to respect the
|
|
293
|
+
Elastic Common Schema (ECS) specification, but without getting too
|
|
294
|
+
strict about it.
|
|
295
|
+
|
|
296
|
+
When the format string contains a dot-separated "path" starting with
|
|
297
|
+
`http.request.`, the rucio formatter will try to extract the given
|
|
298
|
+
path from the flask `request` object.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
def __init__(
|
|
302
|
+
self,
|
|
303
|
+
fmt: Optional[str] = None,
|
|
304
|
+
validate: Optional[bool] = None,
|
|
305
|
+
output_json: bool = False,
|
|
306
|
+
additional_fields: Optional[Mapping[ECS_FIELDS, str]] = None
|
|
307
|
+
):
|
|
308
|
+
_kwargs = {}
|
|
309
|
+
if validate is not None:
|
|
310
|
+
_kwargs["validate"] = validate
|
|
311
|
+
|
|
312
|
+
data_sources: dict[str, LogDataSource] = dict(
|
|
313
|
+
(ecs_field, LogDataSource((ecs_field,), formatter=_get_record_attribute(log_record)))
|
|
314
|
+
for ecs_field, log_record in BUILTIN_FIELDS
|
|
315
|
+
)
|
|
316
|
+
data_sources.update({
|
|
317
|
+
'@timestamp': LogDataSource(('@timestamp',), formatter=_timestamp_formatter),
|
|
318
|
+
'message': MessageLogDataSource(), # ('message', 'error.type', 'error.message', 'error.stack_trace'),
|
|
319
|
+
})
|
|
320
|
+
data_sources.update(
|
|
321
|
+
(ecs_field, LogDataSource((ecs_field,),
|
|
322
|
+
dst_record_attr=_ecs_field_to_record_attribute(ecs_field),
|
|
323
|
+
formatter=_get_request_data(request_path=request_path.split('.'))))
|
|
324
|
+
for ecs_field, request_path in (
|
|
325
|
+
('client.account.name', 'headers.X-Rucio-Account'), # this field is rucio-specific, not from the ECS specification
|
|
326
|
+
('network.forwarded_ip', 'access_route.0'),
|
|
327
|
+
('source.ip', 'remote_addr'),
|
|
328
|
+
('url.full', 'url'),
|
|
329
|
+
('user_agent.original', 'user_agent'),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
if additional_fields:
|
|
333
|
+
data_sources.update({
|
|
334
|
+
ecs_field: ConstantStrDataSource(ecs_field, field_value)
|
|
335
|
+
for ecs_field, field_value in additional_fields.items()
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
self._desired_data_sources = []
|
|
339
|
+
if fmt:
|
|
340
|
+
# extract of field1, field2 from the printf format-string "%(field1)s %(field2)i"
|
|
341
|
+
# Allow simple path-like structures in fields (words separated with dots):
|
|
342
|
+
# - http.request.headers.X-Rucio-Auth-Token
|
|
343
|
+
# - http.request.url
|
|
344
|
+
_format_string_fields = set(t[0] for t in re.findall(r'%\((\w+(.\w+(-\w+)*)*)\)', fmt))
|
|
345
|
+
|
|
346
|
+
for field_name in _format_string_fields:
|
|
347
|
+
data_source = data_sources.get(LOG_RECORD_TO_ECS_MAP.get(field_name, field_name), None)
|
|
348
|
+
|
|
349
|
+
if '.' in field_name:
|
|
350
|
+
dst_record_attr = _ecs_field_to_record_attribute(field_name)
|
|
351
|
+
fmt = fmt.replace(f'%({field_name})', f'%({dst_record_attr})')
|
|
352
|
+
if field_name.startswith('http.request.'):
|
|
353
|
+
path = field_name.replace('http.request.', '', 1).split('.')
|
|
354
|
+
data_source = LogDataSource((field_name,), dst_record_attr=dst_record_attr, formatter=_get_request_data(path))
|
|
355
|
+
elif not data_source:
|
|
356
|
+
data_source = LogDataSource((field_name,), formatter=_get_record_attribute(field_name))
|
|
357
|
+
|
|
358
|
+
if data_source:
|
|
359
|
+
self._desired_data_sources.append(data_source)
|
|
360
|
+
else:
|
|
361
|
+
self._desired_data_sources = [data_sources['message']]
|
|
362
|
+
|
|
363
|
+
self.output_json = output_json
|
|
364
|
+
super().__init__(fmt=fmt, style='%', **_kwargs)
|
|
365
|
+
|
|
366
|
+
def format(self, record: "LogRecord") -> str:
|
|
367
|
+
json_record = dict(itertools.chain.from_iterable(f.format(record) for f in self._desired_data_sources)) # type: ignore
|
|
368
|
+
if self.output_json:
|
|
369
|
+
return self._to_json(_unflatten_dict(json_record))
|
|
370
|
+
else:
|
|
371
|
+
return super().format(record)
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def _to_json(record: dict[str, Any]) -> str:
|
|
375
|
+
try:
|
|
376
|
+
return json.dumps(record, default=_json_serializable)
|
|
377
|
+
except (TypeError, ValueError, OverflowError):
|
|
378
|
+
try:
|
|
379
|
+
return json.dumps(record)
|
|
380
|
+
except (TypeError, ValueError, OverflowError):
|
|
381
|
+
return '{}'
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def rucio_log_formatter(process_name: Optional[str] = None) -> RucioFormatter:
|
|
385
|
+
config_logformat = config_get('common', 'logformat', raise_exception=False, default='%(asctime)s\t%(name)s\t%(process)d\t%(levelname)s\t%(message)s')
|
|
386
|
+
output_json = config_get_bool('common', 'logjson', default=False)
|
|
387
|
+
additional_fields = {}
|
|
388
|
+
if process_name:
|
|
389
|
+
additional_fields['process.name'] = process_name
|
|
390
|
+
return RucioFormatter(fmt=config_logformat, output_json=output_json, additional_fields=additional_fields)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def setup_logging(application: Optional["Flask"] = None, process_name: Optional[str] = None) -> None:
|
|
394
|
+
"""
|
|
395
|
+
Configures the logging by setting the output stream to stdout and
|
|
396
|
+
configures log level and log format.
|
|
397
|
+
"""
|
|
398
|
+
config_loglevel = getattr(logging, config_get('common', 'loglevel', raise_exception=False, default='DEBUG').upper())
|
|
399
|
+
|
|
400
|
+
stdouthandler = logging.StreamHandler(stream=sys.stdout)
|
|
401
|
+
stdouthandler.setFormatter(rucio_log_formatter(process_name=process_name))
|
|
402
|
+
stdouthandler.setLevel(config_loglevel)
|
|
403
|
+
logging.basicConfig(level=config_loglevel, handlers=[stdouthandler])
|
|
404
|
+
|
|
405
|
+
if application:
|
|
406
|
+
application.logger.addHandler(stdouthandler)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def formatted_logger(innerfunc: Callable, formatstr: str = "%s") -> Callable:
|
|
410
|
+
"""
|
|
411
|
+
Decorates the passed function, formatting log input by
|
|
412
|
+
the passed formatstr. The format string must always include a %s.
|
|
413
|
+
|
|
414
|
+
:param innerfunc: function to be decorated. Must take (level, msg) arguments.
|
|
415
|
+
:param formatstr: format string with %s as placeholder.
|
|
416
|
+
"""
|
|
417
|
+
@functools.wraps(innerfunc)
|
|
418
|
+
def log_format(level: int, msg: object, *args, **kwargs) -> Callable:
|
|
419
|
+
return innerfunc(level, formatstr % msg, *args, **kwargs)
|
|
420
|
+
return log_format
|