Skip to content

Commit 8f542f4

Browse files
committed
Adding debug logs to base API as urllib3 logs are not helpful for our purpose.
1 parent cbd30c8 commit 8f542f4

File tree

2 files changed

+95
-1
lines changed

2 files changed

+95
-1
lines changed

c8y_api/_base_api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import json as json_lib
6+
import logging
67
from typing import Union, Dict, BinaryIO
78

89
import collections
@@ -75,6 +76,8 @@ class CumulocityRestApi:
7576
CONTENT_MANAGED_OBJECT = 'application/vnd.com.nsn.cumulocity.managedobject+json'
7677
CONTENT_MEASUREMENT_COLLECTION = 'application/vnd.com.nsn.cumulocity.measurementcollection+json'
7778

79+
log = logging.getLogger(__name__ + '.CumulocityRestApi')
80+
7881
def __init__(self, base_url: str, tenant_id: str, username: str = None, password: str = None,
7982
auth: AuthBase = None, application_key: str = None, processing_mode: str = None):
8083
"""Build a CumulocityRestApi instance.
@@ -204,6 +207,8 @@ def get(self, resource: str, params: dict = None, accept: str = None, ordered: b
204207
"""
205208
additional_headers = self._prepare_headers(accept=accept)
206209
r = self.session.get(self.base_url + resource, params=params, headers=additional_headers)
210+
if self.log.isEnabledFor(logging.DEBUG):
211+
self.log.debug(f'GET {resource} {self._format_params(params)} {r.status_code}')
207212
if r.status_code == 401:
208213
raise UnauthorizedError(self.METHOD_GET, self.base_url + resource)
209214
if r.status_code == 403:
@@ -237,6 +242,8 @@ def get_file(self, resource: str, params: dict = None) -> bytes:
237242
(only 200 is accepted).
238243
"""
239244
r = self.session.get(self.base_url + resource, params=params)
245+
if self.log.isEnabledFor(logging.DEBUG):
246+
self.log.debug(f'GET {resource} {self._format_params(params)} {r.status_code}')
240247
if r.status_code == 401:
241248
raise UnauthorizedError(self.METHOD_GET, self.base_url + resource)
242249
if r.status_code == 403:
@@ -273,6 +280,8 @@ def post(self, resource: str, json: dict, accept: str = None, content_type: str
273280
assert isinstance(json, dict)
274281
additional_headers = self._prepare_headers(accept=accept, content_type=content_type)
275282
r = self.session.post(self.base_url + resource, json=json, headers=additional_headers)
283+
if self.log.isEnabledFor(logging.DEBUG):
284+
self.log.debug(f"POST {resource} '{json_lib.dumps(json)}' {r.status_code}")
276285
if r.status_code == 401:
277286
raise UnauthorizedError(self.METHOD_POST, self.base_url + resource, message=r.json()['message'])
278287
if r.status_code == 403:
@@ -324,6 +333,9 @@ def perform_post(open_file):
324333
else:
325334
r = perform_post(file)
326335

336+
if self.log.isEnabledFor(logging.DEBUG):
337+
self.log.debug(f"POST {resource} '{file}' {self._format_params(object)} {r.status_code}")
338+
327339
if r.status_code == 401:
328340
raise UnauthorizedError(self.METHOD_POST, self.base_url + resource)
329341
if r.status_code == 403:
@@ -362,6 +374,8 @@ def put(self, resource: str, json: dict, params: dict = None,
362374
assert isinstance(json, dict)
363375
additional_headers = self._prepare_headers(accept=accept, content_type=content_type)
364376
r = self.session.put(self.base_url + resource, json=json, params=params, headers=additional_headers)
377+
if self.log.isEnabledFor(logging.DEBUG):
378+
self.log.debug(f"PUT {resource} {self._format_params(params)} '{json_lib.dumps(json)}' {r.status_code}")
365379
if r.status_code == 401:
366380
raise UnauthorizedError(self.METHOD_PUT, self.base_url + resource)
367381
if r.status_code == 403:
@@ -413,6 +427,8 @@ def read_file_data(f):
413427
additional_headers = self._prepare_headers(accept=accept, content_type=content_type)
414428
data = read_file_data(file)
415429
r = self.session.put(self.base_url + resource, data=data, headers=additional_headers)
430+
if self.log.isEnabledFor(logging.DEBUG):
431+
self.log.debug(f"PUT {resource} '{file}' {r.status_code}")
416432
if r.status_code == 401:
417433
raise UnauthorizedError(self.METHOD_PUT, self.base_url + resource)
418434
if r.status_code == 403:
@@ -448,6 +464,8 @@ def delete(self, resource: str, json: dict = None, params: dict = None):
448464
if json:
449465
assert isinstance(json, dict)
450466
r = self.session.delete(self.base_url + resource, json=json, params=params, headers={'Accept': None})
467+
if self.log.isEnabledFor(logging.DEBUG):
468+
self.log.debug(f"DELETE {resource} {self._format_params(params)} '{json_lib.dumps(json)}' {r.status_code}")
451469
if r.status_code == 401:
452470
raise UnauthorizedError(self.METHOD_DELETE, self.base_url + resource)
453471
if r.status_code == 403:
@@ -496,6 +514,14 @@ def format_value(value):
496514

497515
return {cls._format_header_key(key): format_value(value) for key, value in kwargs.items() if value is not None}
498516

517+
@staticmethod
518+
def _format_params(params):
519+
if not params:
520+
return '-'
521+
return ', '.join(
522+
[f"{k}={v}" for k, v in params.items()]
523+
)
524+
499525
@staticmethod
500526
def _format_header_key(key: str) -> str:
501527
"""Format a snake_case argument name into a proper Header-Name.

tests/test_base_api.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import base64
66
from typing import Type
7-
from unittest.mock import patch, Mock
7+
from unittest.mock import patch, Mock, PropertyMock
88

99
import json
1010
import pytest
@@ -18,6 +18,7 @@
1818
AccessDeniedError,
1919
HttpError
2020
)
21+
from tests.utils import isolate_last_call_arg, assert_all_in_any, assert_all_not_in_any
2122

2223

2324
@pytest.fixture(scope='function')
@@ -351,3 +352,70 @@ def test_empty_response(mock_c8y: CumulocityRestApi):
351352
url=mock_c8y.base_url + '/resource',
352353
status=200)
353354
mock_c8y.put('/resource', json={})
355+
356+
357+
@pytest.mark.parametrize(
358+
'method, kwargs, expected, not_expected',
359+
[
360+
('get', {"params": {"a": 1}}, ['a=1'], ['{']),
361+
('post', {"json": {"b": 2}}, ['{"b": 2}'], []),
362+
('put', {"params": {"a": 1}, "json": {"b": 2}}, ['a=1', '{"b": 2}'], []),
363+
('delete', {"params": {"a": 1}, "json": {"b": 2}}, ['a=1', '{"b": 2}'], []),
364+
],
365+
ids=[
366+
"get",
367+
"post",
368+
"put",
369+
"delete",
370+
]
371+
)
372+
@patch('c8y_api.CumulocityRestApi.session', new_callable=PropertyMock)
373+
def test_request_logging_simple(session_property_mock, mock_c8y: CumulocityRestApi,
374+
method, kwargs, expected, not_expected):
375+
"""Verify that requests are logged properly."""
376+
377+
session_mock = Mock(name='my-session-mock')
378+
getattr(session_mock, method).return_value = Mock(status_code=200)
379+
session_property_mock.return_value = session_mock
380+
mock_c8y.log = Mock(isEnabledFor=Mock(return_value=True))
381+
382+
getattr(mock_c8y, method)('/resource', **kwargs)
383+
mock_c8y.log.debug.assert_called_once()
384+
msg = isolate_last_call_arg(mock_c8y.log.debug, 0)
385+
assert_all_in_any([method.upper(), '/resource', *expected], msg)
386+
assert_all_not_in_any(not_expected, msg)
387+
388+
389+
@patch('c8y_api.CumulocityRestApi.session', new_callable=PropertyMock)
390+
def test_request_logging_file(session_property_mock, mock_c8y: CumulocityRestApi, ):
391+
"""Verify that requests are logged properly."""
392+
393+
session_mock = Mock(name='my-session-mock')
394+
session_property_mock.return_value = session_mock
395+
mock_c8y.log = Mock(isEnabledFor=Mock(return_value=True))
396+
397+
# GET File
398+
session_mock.get.return_value = Mock(status_code=200)
399+
mock_c8y.log.debug.reset_mock()
400+
mock_c8y.get_file('/resource', params={'a': 1})
401+
mock_c8y.log.debug.assert_called_once()
402+
msg = isolate_last_call_arg(mock_c8y.log.debug, 0)
403+
assert_all_in_any(['/resource', 'GET', 'a=1'], msg)
404+
405+
# POST File
406+
mock_c8y.log.debug.reset_mock()
407+
session_mock.post.return_value = Mock(status_code=201) # posting file should give 201
408+
with patch("builtins.open"):
409+
mock_c8y.post_file('/resource', file='filename', object={'a': 1})
410+
mock_c8y.log.debug.assert_called_once()
411+
msg = isolate_last_call_arg(mock_c8y.log.debug, 0)
412+
assert_all_in_any(['/resource', 'POST', 'a=1', 'filename'], msg)
413+
414+
# PUT File
415+
mock_c8y.log.debug.reset_mock()
416+
session_mock.put.return_value = Mock(status_code=201) # putting file should give 201
417+
with patch("builtins.open"):
418+
mock_c8y.put_file('/resource', file='filename')
419+
mock_c8y.log.debug.assert_called_once()
420+
msg = isolate_last_call_arg(mock_c8y.log.debug, 0)
421+
assert_all_in_any(['/resource', 'PUT', 'filename'], msg)

0 commit comments

Comments
 (0)