Skip to content

Commit c4064d5

Browse files
authored
Merge pull request #176 from reportportal/logging-context
Logging context
2 parents a3baaef + 0664393 commit c4064d5

File tree

10 files changed

+407
-36
lines changed

10 files changed

+407
-36
lines changed

reportportal_client/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
limitations under the License.
1515
"""
1616

17+
from ._local import current
18+
from .logs import RPLogger, RPLogHandler
1719
from .service import ReportPortalService
1820
from .steps import step
19-
from ._local import current
2021

2122
__all__ = [
23+
'current',
24+
"RPLogger",
25+
"RPLogHandler",
2226
'ReportPortalService',
2327
'step',
24-
'current'
2528
]

reportportal_client/client.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def __init__(self,
5757
retries=None,
5858
max_pool_size=50,
5959
launch_id=None,
60+
http_timeout=(10, 10),
6061
**_):
6162
"""Initialize required attributes.
6263
@@ -70,6 +71,11 @@ def __init__(self,
7071
:param verify_ssl: Option to skip ssl verification
7172
:param max_pool_size: Option to set the maximum number of
7273
connections to save the pool.
74+
:param launch_id: a launch id to use instead of starting own
75+
one
76+
:param http_timeout: a float in seconds for connect and read
77+
timeout. Use a Tuple to specific connect
78+
and read separately.
7379
"""
7480
set_current(self)
7581
self._batch_logs = []
@@ -85,6 +91,7 @@ def __init__(self,
8591
self.log_batch_size = log_batch_size
8692
self.token = token
8793
self.verify_ssl = verify_ssl
94+
self.http_timeout = http_timeout
8895
self.session = requests.Session()
8996
self.step_reporter = StepReporter(self)
9097
self._item_stack = []

reportportal_client/client.pyi

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,22 @@ class RPClient:
2323
project: Text = ...
2424
token: Text = ...
2525
verify_ssl: bool = ...
26+
http_timeout: Union[float, Tuple[float, float]] = ...
2627
session: Session = ...
2728
step_reporter: StepReporter = ...
2829

29-
def __init__(self,
30-
endpoint: Text,
31-
project: Text, token: Text,
32-
log_batch_size: int = ...,
33-
is_skipped_an_issue: bool = ...,
34-
verify_ssl: bool = ...,
35-
retries: int = ...,
36-
max_pool_size: int = ...,
37-
launch_id: Text = ...) -> None: ...
30+
def __init__(
31+
self,
32+
endpoint: Text,
33+
project: Text, token: Text,
34+
log_batch_size: int = ...,
35+
is_skipped_an_issue: bool = ...,
36+
verify_ssl: bool = ...,
37+
retries: int = ...,
38+
max_pool_size: int = ...,
39+
launch_id: Text = ...,
40+
http_timeout: Union[float, Tuple[float, float]] = ...
41+
) -> None: ...
3842

3943
def finish_launch(self,
4044
end_time: Text,

reportportal_client/core/rp_requests.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,30 +42,35 @@ class HttpRequest:
4242
"""This model stores attributes related to RP HTTP requests."""
4343

4444
def __init__(self, session_method, url, data=None, json=None,
45-
files=None, verify_ssl=True):
45+
files=None, verify_ssl=True, http_timeout=(10, 10)):
4646
"""Initialize instance attributes.
4747
4848
:param session_method: Method of the requests.Session instance
4949
:param url: Request URL
5050
:param data: Dictionary, list of tuples, bytes, or file-like
5151
object to send in the body of the request
52-
:param json: JSON to be send in the body of the request
53-
:param verify: Is certificate verification required
52+
:param json: JSON to be sent in the body of the request
53+
:param verify_ssl: Is SSL certificate verification required
54+
:param http_timeout: a float in seconds for connect and read
55+
timeout. Use a Tuple to specific connect and
56+
read separately.
5457
"""
5558
self.data = data
5659
self.files = files
5760
self.json = json
5861
self.session_method = session_method
5962
self.url = url
6063
self.verify_ssl = verify_ssl
64+
self.http_timeout = http_timeout
6165

6266
def make(self):
6367
"""Make HTTP request to the Report Portal API."""
6468
for attempt in range(SEND_RETRY_COUNT):
6569
try:
6670
return RPResponse(self.session_method(
6771
self.url, data=self.data, json=self.json,
68-
files=self.files, verify=self.verify_ssl)
72+
files=self.files, verify=self.verify_ssl,
73+
timeout=self.http_timeout)
6974
)
7075
# https://github.com/reportportal/client-Python/issues/39
7176
except KeyError:

reportportal_client/core/rp_requests.pyi

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ from reportportal_client.core.rp_issues import Issue as Issue
33
from reportportal_client.core.rp_responses import RPResponse as RPResponse
44
from reportportal_client.static.abstract import AbstractBaseClass
55
from reportportal_client.static.defines import Priority as Priority
6-
from typing import Any, Callable, ByteString, Dict, IO, List, Optional, Text, Union
6+
from typing import Any, Callable, ByteString, Dict, IO, List, Optional, Text, \
7+
Union, Tuple
8+
79

810
class HttpRequest:
911
session_method: Callable = ...
1012
url: Text = ...
11-
files: Optional[Dict]
12-
data = Optional[Union[Dict, List[Union[tuple, ByteString]], IO]] = ...
13-
json = Optional[Dict] = ...
14-
verify_ssl = Optional[bool]
13+
files: Optional[Dict] = ...
14+
data: Optional[Union[Dict, List[Union[tuple, ByteString]], IO]] = ...
15+
json: Optional[Dict] = ...
16+
verify_ssl: Optional[bool] = ...
17+
http_timeout: Union[float, Tuple[float, float]] = ...
1518
def __init__(self,
1619
session_method: Callable,
1720
url: Text,

reportportal_client/core/worker.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,15 @@ def __perform_stop(self, stop_command):
179179
if not self.is_alive():
180180
# Already stopped or already dead or not even started
181181
return
182-
if not self._stop_lock.acquire(blocking=False):
182+
with self._stop_lock:
183+
if not self.is_alive():
184+
# Already stopped by previous thread
185+
return
183186
self.send(stop_command)
187+
# Do not release main thread until worker process all requests,
188+
# since main thread might forcibly quit python interpreter as in
189+
# pytest
184190
self._stop_lock.wait(THREAD_TIMEOUT)
185-
self._stop_lock.release()
186191

187192
def stop(self):
188193
"""Stop the worker.

reportportal_client/logs/__init__.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Copyright (c) 2022 https://reportportal.io .
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# https://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License
13+
"""Report portal logging handling module."""
14+
15+
import logging
16+
import sys
17+
18+
from six import PY2
19+
from six.moves.urllib.parse import urlparse
20+
21+
from reportportal_client._local import current
22+
from reportportal_client.helpers import timestamp
23+
24+
25+
class RPLogger(logging.getLoggerClass()):
26+
"""RPLogger class for low-level logging in tests."""
27+
28+
def __init__(self, name, level=0):
29+
"""
30+
Initialize RPLogger instance.
31+
32+
:param name: logger name
33+
:param level: level of logs
34+
"""
35+
super(RPLogger, self).__init__(name, level=level)
36+
37+
def _log(self, level, msg, args,
38+
exc_info=None, extra=None, stack_info=False, attachment=None):
39+
"""
40+
Low-level logging routine which creates a LogRecord and then calls.
41+
42+
all the handlers of this logger to handle the record
43+
:param level: level of log
44+
:param msg: message in log body
45+
:param args: additional args
46+
:param exc_info: system exclusion info
47+
:param extra: extra info
48+
:param stack_info: stacktrace info
49+
:param attachment: attachment file
50+
"""
51+
sinfo = None
52+
if logging._srcfile:
53+
# IronPython doesn't track Python frames, so findCaller raises an
54+
# exception on some versions of IronPython. We trap it here so that
55+
# IronPython can use logging.
56+
try:
57+
if PY2:
58+
# In python2.7 findCaller() don't accept any parameters
59+
# and returns 3 elements
60+
fn, lno, func = self.findCaller()
61+
else:
62+
fn, lno, func, sinfo = self.findCaller(stack_info)
63+
64+
except ValueError: # pragma: no cover
65+
fn, lno, func = '(unknown file)', 0, '(unknown function)'
66+
else:
67+
fn, lno, func = '(unknown file)', 0, '(unknown function)'
68+
69+
if exc_info and not isinstance(exc_info, tuple):
70+
exc_info = sys.exc_info()
71+
72+
if PY2:
73+
# In python2.7 makeRecord() accepts everything but sinfo
74+
record = self.makeRecord(self.name, level, fn, lno, msg, args,
75+
exc_info, func, extra)
76+
else:
77+
record = self.makeRecord(self.name, level, fn, lno, msg, args,
78+
exc_info, func, extra, sinfo)
79+
80+
if not getattr(record, 'attachment', None):
81+
record.attachment = attachment
82+
self.handle(record)
83+
84+
85+
class RPLogHandler(logging.Handler):
86+
"""RPLogHandler class for logging tests."""
87+
88+
# Map loglevel codes from `logging` module to ReportPortal text names:
89+
_loglevel_map = {
90+
logging.NOTSET: 'TRACE',
91+
logging.DEBUG: 'DEBUG',
92+
logging.INFO: 'INFO',
93+
logging.WARNING: 'WARN',
94+
logging.ERROR: 'ERROR',
95+
logging.CRITICAL: 'ERROR',
96+
}
97+
_sorted_levelnos = sorted(_loglevel_map.keys(), reverse=True)
98+
99+
def __init__(self, level=logging.NOTSET, filter_client_logs=False,
100+
endpoint=None,
101+
ignored_record_names=tuple('reportportal_client')):
102+
"""
103+
Initialize RPLogHandler instance.
104+
105+
:param level: level of logging
106+
:param filter_client_logs: if True throw away logs emitted by a
107+
ReportPortal client
108+
:param endpoint: link to send reports
109+
:param ignored_record_names: a tuple of record names which will be
110+
filtered out by the handler (with startswith method)
111+
"""
112+
super(RPLogHandler, self).__init__(level)
113+
self.filter_client_logs = filter_client_logs
114+
self.ignored_record_names = ignored_record_names
115+
self.endpoint = endpoint
116+
117+
def filter(self, record):
118+
"""Filter specific records to avoid sending those to RP.
119+
120+
:param record: A log record to be filtered
121+
:return: False if the given record does no fit for sending
122+
to RP, otherwise True.
123+
"""
124+
if not self.filter_client_logs:
125+
return True
126+
if record.name.startswith(self.ignored_record_names):
127+
return False
128+
if record.name.startswith('urllib3.connectionpool'):
129+
# Filter the reportportal_client requests instance
130+
# urllib3 usage
131+
hostname = urlparse(self.endpoint).hostname
132+
if hostname in self.format(record):
133+
return False
134+
return True
135+
136+
def emit(self, record):
137+
"""
138+
Emit function.
139+
140+
:param record: a log Record of requests
141+
:return: log ID
142+
"""
143+
msg = ''
144+
145+
try:
146+
msg = self.format(record)
147+
except (KeyboardInterrupt, SystemExit):
148+
raise
149+
except Exception:
150+
self.handleError(record)
151+
152+
for level in self._sorted_levelnos:
153+
if level <= record.levelno:
154+
rp_client = current()
155+
return rp_client.log(
156+
timestamp(),
157+
msg,
158+
level=self._loglevel_map[level],
159+
attachment=getattr(record, 'attachment'),
160+
item_id=rp_client.current_item()
161+
)

0 commit comments

Comments
 (0)