Skip to content
This repository has been archived by the owner on Jul 28, 2023. It is now read-only.

Improve logging #579

Merged
merged 54 commits into from
Mar 17, 2020
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
0f318a9
Setup logger for package.
Mar 4, 2020
5e4b710
Add job submission logging messages.
Mar 5, 2020
43480d4
revise log message
Mar 5, 2020
d679496
Merge branch 'master' into improve-logging
Mar 5, 2020
461fa95
review comments
Mar 5, 2020
3b55797
update docstring.
Mar 6, 2020
742aee2
seperate console and file logging.
Mar 9, 2020
4eec085
remove logging from rest endpoints.
Mar 9, 2020
ce535ce
add logging to session.request
Mar 9, 2020
c16b442
refactor setup_logger
Mar 9, 2020
3a10fb9
update docstring
Mar 9, 2020
df7d67f
resolve merge conflicts
Mar 9, 2020
678b8b0
censor the apiToken
Mar 9, 2020
a0f397b
use appropriate formatting for logging
Mar 9, 2020
8ff3823
resolve merge conflicts.
Mar 10, 2020
0f8dff9
review comments
Mar 10, 2020
e3b6045
write tests for logger.
Mar 10, 2020
a867df2
fix lint
Mar 10, 2020
42fed5c
update docstrings.
Mar 11, 2020
c14b699
add response data
Mar 11, 2020
a38d0ed
Merge branch 'master' into improve-logging
Mar 11, 2020
7aa0541
update changelog
Mar 11, 2020
6ee98e2
style fixes
Mar 11, 2020
6364d6b
update changelog
Mar 11, 2020
4ed6177
review comments for tests.
Mar 11, 2020
7654786
move log level setting placement.
Mar 11, 2020
2e66d4e
update docs
Mar 11, 2020
f0831c2
add chained exception logging.
Mar 12, 2020
0e216d7
Use NamedTemporaryFile for log file test.
Mar 12, 2020
c7ec836
fix style.
Mar 12, 2020
9f55308
cleanup methods.
Mar 12, 2020
0da5791
fiter websocket response for logging.
Mar 12, 2020
7eeef24
filter session request information.
Mar 12, 2020
0a3f75b
Merge branch 'master' into improve-logging
Mar 12, 2020
2617b73
filter device name from job sumission response data
Mar 12, 2020
d30f24c
update docstrings.
Mar 12, 2020
88de18b
change param name
Mar 13, 2020
7c855a8
make substring test subTest
Mar 13, 2020
668ece4
Fix regex and add more context
Mar 13, 2020
ec8e988
resolve merge conflicts.
Mar 16, 2020
325e4f9
review changes
Mar 16, 2020
5d9aa36
Avoid making multiple status calls least_busy
Mar 16, 2020
2d46528
only log job submission for post requests
Mar 16, 2020
9e1adbc
Merge branch 'master' into improve-logging
Mar 16, 2020
c93d1dd
remove duplicate retrieve_job
Mar 16, 2020
cbfd71e
filter out GET request data.
Mar 17, 2020
5275631
change level to getEffectiveLevel.
Mar 17, 2020
2cc0873
Refactor log filtering in session.
Mar 17, 2020
8fc350d
reuse LOG_LEVEL if provider logger not set
Mar 17, 2020
b458f95
fix lint
Mar 17, 2020
57af448
Do not log request_data for certain urls.
Mar 17, 2020
7def1f1
refactor logging filtering in session.
Mar 17, 2020
33e0a10
review comments.
Mar 17, 2020
b9606bb
move log messages before object storage requests.
Mar 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ The format is based on [Keep a Changelog].
an exception is raised. Also, `IBMQBackend.status()` now raises a
`IBMQBackendApiProtocolError` exception, instead of a general `LookupError`,
if there was an issue with validating the status. (\#572)
- The IBM Quantum Provider now has a logger component named `qiskit.providers.ibmq`.
The logger's level may be set using the `QSIKIT_IBMQ_PROVIDER_LOG_LEVEL`
environment variable. Also, logged messages may be written to a file, instead of
the screen, using the `QISKIT_IBMQ_PROVIDER_LOG_FILE` environment variable. (\#579)
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved

## [0.5.0] - 2020-02-26

Expand Down
18 changes: 18 additions & 0 deletions qiskit/providers/ibmq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@

Modules representing the IBM Quantum Provider.

Environment Variables
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved
=====================
The following environment variables are used by the IBM Quantum Provider:

* ``QISKIT_IBMQ_PROVIDER_LOG_LEVEL``: Specifies the log level to use, for the
provider modules, when logging to the screen. If an invalid level is set, the
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved
log level defaults to ``WARNING``. The valid log levels are ``DEBUG``, ``INFO``,
``WARNING``, ``ERROR``, and ``CRITICAL`` (case-insensitive).
* ``QISKIT_IBMQ_PROVIDER_LOG_FILE``: Specifies the name of the logfile to create,
which will contain the logs. If it is not specified, a logfile will not be created,
but the message will still be logged to the screen.
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved

Functions
=========
.. autosummary::
Expand Down Expand Up @@ -58,6 +70,7 @@
IBMQProviderError
"""

import logging
from typing import List

from .ibmqfactory import IBMQFactory
Expand All @@ -68,9 +81,14 @@
from .backendjoblimit import BackendJobLimit
from .exceptions import *
from .ibmqbackendservice import IBMQBackendService
from .utils.utils import setup_logger

from .version import __version__

# Setup the logger for the IBM Quantum Provider package.
logger = logging.getLogger(__name__)
setup_logger(logger)

IBMQ = IBMQFactory()
"""A global instance of an account manager that is used as the entry point for convenience."""

Expand Down
3 changes: 1 addition & 2 deletions qiskit/providers/ibmq/api/clients/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,7 @@ def get_job_status(
timeout = original_timeout - (time.time() - start_time)
else:
response_raw = yield from websocket.recv()
logger.debug('Received message from websocket: %s',
response_raw)
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved
logger.debug('Received message from websocket: %s', response_raw)

response = WebsocketResponseMethod.from_bytes(response_raw)
last_status = response.data
Expand Down
5 changes: 5 additions & 0 deletions qiskit/providers/ibmq/api/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""Session customized for IBM Quantum Experience access."""

import os
import logging
from typing import Dict, Optional, Any, Tuple, Union
from requests import Session, RequestException, Response
from requests.adapters import HTTPAdapter
Expand All @@ -31,6 +32,7 @@
)
CLIENT_APPLICATION = 'ibmqprovider/' + ibmq_provider_version
CUSTOM_HEADER_ENV_VAR = 'QE_CUSTOM_CLIENT_APP_HEADER'
logger = logging.getLogger(__name__)


class PostForcelistRetry(Retry):
Expand Down Expand Up @@ -218,6 +220,9 @@ def request( # type: ignore[override]
try:
response = super().request(method, final_url, **kwargs)
response.raise_for_status()
# Only log if the endpoint is not `/users` or `/version`
if not url.startswith(('/users', '/version')):
logger.debug('Endpoint: %s. Method: %s. Request Data: %s.', url, method, kwargs)
except RequestException as ex:
# Wrap the requests exceptions into a IBM Q custom one, for
# compatibility.
Expand Down
1 change: 1 addition & 0 deletions qiskit/providers/ibmq/ibmqbackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ def _submit_job(
})
try:
job = IBMQJob.from_dict(submit_info)
logger.debug('Job %s successfully submitted to backend %s.', job.job_id(), self.name())
except ModelValidationError as err:
raise IBMQBackendApiProtocolError('Unexpected return value received from the server '
'when submitting job: {}'.format(str(err))) from err
Expand Down
2 changes: 2 additions & 0 deletions qiskit/providers/ibmq/job/ibmqjob.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ def cancel(self) -> bool:
try:
response = self._api.job_cancel(self.job_id())
self._cancelled = get_cancel_status(response)
logger.debug('Job %s cancel status is "%s". Response data: %s.',
self.job_id(), self._cancelled, response)
return self._cancelled
except ApiError as error:
self._cancelled = False
Expand Down
46 changes: 46 additions & 0 deletions qiskit/providers/ibmq/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@

"""General utility functions."""

import os
import re
import logging
import keyword
from typing import List, Optional, Type
from logging import Logger


def to_python_identifier(name: str) -> str:
Expand Down Expand Up @@ -56,3 +59,46 @@ def validate_job_tags(job_tags: Optional[List[str]], exception: Type[Exception])
if job_tags and (not isinstance(job_tags, list) or
not all(isinstance(tag, str) for tag in job_tags)):
raise exception("job_tags needs to be a list or strings.")


def setup_logger(logger: Logger) -> None:
"""Setup the logger for the provider modules with the appropriate level.

It involves:
* Use the `QISKIT_IBMQ_PROVIDER_LOG_LEVEL` environment variable to
determine the log level for the provider modules. If it is set, the
provider uses the specified level. If it is set to an invalid level,
the log level defaults to `WARNING`. The valid log levels are ``DEBUG``,
``INFO``, ``WARNING``, ``ERROR``, and ``CRITICAL`` (case-insensitive).
* Use the `QISKIT_IBMQ_PROVIDER_LOG_FILE` environment variable to
specify the filename to use for logging the logs to a file. If it is
not set, the logs will not be logged to a file, but the messages will
still be logged to the screen.
"""
log_level = os.getenv('QISKIT_IBMQ_PROVIDER_LOG_LEVEL', '')
log_file = os.getenv('QISKIT_IBMQ_PROVIDER_LOG_FILE', '')

# Set the logging level, if specified.
if log_level:
# Default to `WARNING` if the specified level is not valid.
level = logging.getLevelName(log_level.upper())
if not isinstance(level, int):
level = logging.WARNING
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved
logger.setLevel(level)

# Setup the formatter for the log messages.
log_fmt = ('{}.%(module)s.%(funcName)s:%(levelname)s:%(asctime)s:'
' %(message)s'.format(logger.name))
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved
formatter = logging.Formatter(log_fmt)

lgarc15 marked this conversation as resolved.
Show resolved Hide resolved
# Log messages to a file (if specified), otherwise log to the screen (default).
if log_file:
# Setup the file handler.
file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved
else:
# Setup the stream handler, for logging to console, with the given format.
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
177 changes: 177 additions & 0 deletions test/ibmq/test_ibmq_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# -*- coding: utf-8 -*-

# This code is part of Qiskit.
#
# (C) Copyright IBM 2020.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Tests related to logger setup via ``setup_logger()``."""

import os
import logging
from unittest.mock import patch

from qiskit.providers.ibmq.utils.utils import setup_logger

from ..ibmqtestcase import IBMQTestCase


class TestLogger(IBMQTestCase):
"""Tests related to logger setup via ``setup_logger()``."""
log_level_env = 'QISKIT_IBMQ_PROVIDER_LOG_LEVEL'
log_file_env = 'QISKIT_IBMQ_PROVIDER_LOG_FILE'
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved

@classmethod
def setUpClass(cls):
"""Get a logger to test. Also, cache the initial log level and a file name.

Note:
The initial log level is cached in order to restore the log level
back to the original in `tearDown`.
"""
cls.logger = logging.getLogger(__name__)
cls.initial_logger_level = cls.logger.level
cls.log_file = '{}.log'.format(cls.logger.name)

def tearDown(self):
"""Restore the original log level and remove the logger handlers.

Note:
This essentially "resets" the logger, so it is set back into its
initial state again for each test.
"""
self.logger.setLevel(self.initial_logger_level)
for handler in self.logger.handlers:
handler.close()
self.logger.handlers.clear()

def test_no_log_level(self):
"""Test setting up a logger without a log level.

Note:
The log level should default to `NOTSET` when no level is specified.
"""
default_level_not_set = logging.NOTSET

with patch.dict('os.environ'):
if self.log_level_env in os.environ:
del os.environ[self.log_level_env]
setup_logger(self.logger)
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(self.logger.level, default_level_not_set,
'The logger level was set to {}, but it should '
'be {}'.format(self.logger.level, logging.NOTSET))

def test_empty_log_level(self):
"""Test setting up a logger with an empty string log level.

Note:
The log level should default to `NOTSET` when an empty string is specified.
"""
default_level_not_set = logging.NOTSET

with patch.dict('os.environ', {self.log_level_env: ''}):
setup_logger(self.logger)
self.assertEqual(self.logger.level, default_level_not_set,
'The logger level was set to {}, but it should '
'be {}.'.format(self.logger.level, logging.NOTSET))

def test_invalid_log_level(self):
"""Test setting up a logger with invalid log levels, should default to `WARNING`.

Note:
The log level should default to `WARNING` when an invalid level is specified.
"""
default_level_invalid = logging.WARNING

invalid_log_levels = ['invalid', 'debugs', 'WarNinGs']
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved
for invalid_log_level in invalid_log_levels:
with patch.dict('os.environ', {self.log_level_env: invalid_log_level}):
setup_logger(self.logger)
self.assertEqual(self.logger.level, default_level_invalid,
'The logger level was set to {}, but it should '
'be {}.'.format(self.logger.level, logging.WARNING))

def test_mixed_casing_log_level(self):
"""Test setting up a logger with valid log levels, case insensitive."""
level_to_set = logging.INFO
valid_log_levels = ['INFO', 'info', 'InFo']

for valid_log_level in valid_log_levels:
with patch.dict('os.environ', {self.log_level_env: valid_log_level}):
setup_logger(self.logger)
self.assertEqual(self.logger.level, level_to_set,
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved
'The logger level was set to {}, but it should '
'be {}.'.format(self.logger.level, logging.INFO))

def test_valid_log_levels(self):
"""Test setting up a logger with all valid levels, case insensitive."""
all_valid_log_levels = {
'debug': logging.DEBUG, 'iNFo': logging.INFO,
'WARNING': logging.WARNING, 'error': logging.ERROR,
'CRITICAL': logging.CRITICAL}
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved

for level_name, level_value in all_valid_log_levels.items():
with patch.dict('os.environ', {self.log_level_env: level_name}):
setup_logger(self.logger)
self.assertEqual(self.logger.level, level_value,
'The logger level was set to {}, but it should '
'be {}.'.format(self.logger.level, level_value))

def test_log_file(self):
"""Test setting up a logger by specifying a file and log level."""
log_level_warning = ('WARNING', logging.WARNING)
env_vars_to_patch = {self.log_level_env: log_level_warning[0],
self.log_file_env: self.log_file}

with patch.dict('os.environ', env_vars_to_patch):
setup_logger(self.logger)
self.assertEqual(self.logger.level, log_level_warning[1],
'The logger level was set to {}, but it should '
'be {}.'.format(self.logger.level, log_level_warning[1]))

# Assert the file handler was created.
self.assertTrue(self.logger.handlers,
'A file handler should have been setup, but it was not.')
self.assertEqual(len(self.logger.handlers), 1,
'Many handlers were setup {}, but it should have only '
'been one.'.format(self.logger.handlers))

# Note that only messages >= `WARNING` will be logged.
self.logger.debug('This is a debug message that should not be logged in the file.')
self.logger.warning('This is a warning message that should be logged in the file.')
self.logger.error('This is a error message that should be logged in the file.')

# Retrieve the file handler.
file_handler = self.logger.handlers[0]
file_path = file_handler.baseFilename

# Assert the file exists.
self.assertTrue(os.path.exists(file_path),
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved
'The file {} does not exist.'.format(file_path))

# Assert the messages were logged.
with open(file_path) as file_:
content_as_str = file_.read()
substrings_to_check = {'debug message': False, 'warning message': True,
'error message': True}

# Check whether the appropriate substrings are in the file.
for substring, in_file in substrings_to_check.items():
if in_file:
self.assertIn(substring, content_as_str,
'The substring "{}" was not found in the file {}({}).'
.format(substring, self.log_file, file_path))
else:
self.assertNotIn(substring, content_as_str,
'The substring "{}" was found in the file {}({}).'
.format('debug message', self.log_file, file_path))

# Delete the file.
os.remove(file_path)
lgarc15 marked this conversation as resolved.
Show resolved Hide resolved