Skip to content

Commit 24ca952

Browse files
Jenkinsopenstack-gerrit
authored andcommitted
Merge "Set up every time record log in file"
2 parents 7f24818 + e23dd6d commit 24ca952

6 files changed

Lines changed: 479 additions & 15 deletions

File tree

‎doc/source/configuration.rst‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,74 @@ Debugging
143143
You may find the :doc:`config show <command-objects/config>`
144144
helpful to debug configuration issues. It will display your current
145145
configuration.
146+
147+
Logging Settings
148+
----------------
149+
150+
By setting `log_level` or `log_file` in the configuration
151+
:file:`clouds.yaml`, a user may enable additional logging::
152+
153+
clouds:
154+
devstack:
155+
auth:
156+
auth_url: http://192.168.122.10:35357/
157+
project_name: demo
158+
username: demo
159+
password: 0penstack
160+
region_name: RegionOne
161+
operation_log:
162+
logging: TRUE
163+
file: /tmp/openstackclient_demo.log
164+
level: info
165+
ds-admin:
166+
auth:
167+
auth_url: http://192.168.122.10:35357/
168+
project_name: admin
169+
username: admin
170+
password: 0penstack
171+
region_name: RegionOne
172+
log_file: /tmp/openstackclient_admin.log
173+
log_level: debug
174+
175+
:dfn:`log_file`: ``</path/file-name>``
176+
Full path to logging file.
177+
:dfn:`log_level`: ``error`` | ``info`` | ``debug``
178+
If log level is not set, ``warning`` will be used.
179+
180+
If log level is ``info``, the following information is recorded:
181+
182+
* cloud name
183+
* user name
184+
* project name
185+
* CLI start time (logging start time)
186+
* CLI end time
187+
* CLI arguments
188+
* CLI return value
189+
* and any ``info`` messages.
190+
191+
If log level is ``debug``, the following information is recorded:
192+
193+
* cloud name
194+
* user name
195+
* project name
196+
* CLI start time (logging start time)
197+
* CLI end time
198+
* CLI arguments
199+
* CLI return value
200+
* API request header/body
201+
* API response header/body
202+
* and any ``debug`` messages.
203+
204+
When a command is executed, these logs are saved every time. Recording the user
205+
operations can help to identify resource changes and provide useful information
206+
for troubleshooting.
207+
208+
If saving the output of a single command use the `--log-file` option instead.
209+
210+
* `--log-file <LOG_FILE>`
211+
212+
The logging level for `--log-file` can be set by using following options.
213+
214+
* `-v, --verbose`
215+
* `-q, --quiet`
216+
* `--debug`

‎doc/source/man/openstack.rst‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ OPTIONS
123123
:option:`--os-interface` <interface>
124124
Interface type. Valid options are `public`, `admin` and `internal`.
125125

126+
:option:`--log-file` <LOGFILE>
127+
Specify a file to log output. Disabled by default.
128+
129+
:option:`-v, --verbose`
130+
Increase verbosity of output. Can be repeated.
131+
132+
:option:`-q, --quiet`
133+
suppress output except warnings and errors
134+
135+
:option:`--debug`
136+
show tracebacks on errors and set verbosity to debug
137+
126138
COMMANDS
127139
========
128140

@@ -240,6 +252,15 @@ When :option:`--os-token` and :option:`--os-url` are both present the
240252
:option:`--os-auth-url` and :option:`--os-username` are present ``password``
241253
auth type is selected.
242254

255+
Logging Settings
256+
----------------
257+
258+
:program:`openstack` can record the operation history by logging settings
259+
in configuration file. Recording the user operation, it can identify the
260+
change of the resource and it becomes useful information for troubleshooting.
261+
262+
See :doc:`../configuration` about Logging Settings for more details.
263+
243264

244265
NOTES
245266
=====

‎openstackclient/common/context.py‎

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
#
13+
14+
"""Context and Formatter"""
15+
16+
import logging
17+
18+
_LOG_MESSAGE_FORMAT = ('%(asctime)s.%(msecs)03d %(process)d '
19+
'%(levelname)s %(name)s [%(clouds_name)s '
20+
'%(username)s %(project_name)s] %(message)s')
21+
_LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
22+
23+
24+
def setup_handler_logging_level(handler_type, level):
25+
"""Setup of the handler for set the logging level
26+
27+
:param handler_type: type of logging handler
28+
:param level: logging level
29+
:return: None
30+
"""
31+
# Set the handler logging level of FileHandler(--log-file)
32+
# and StreamHandler
33+
for h in logging.getLogger('').handlers:
34+
if type(h) is handler_type:
35+
h.setLevel(level)
36+
37+
38+
def setup_logging(shell, cloud_config):
39+
"""Get one cloud configuration from configuration file and setup logging
40+
41+
:param shell: instance of openstackclient shell
42+
:param cloud_config:
43+
instance of the cloud specified by --os-cloud
44+
in the configuration file
45+
:return: None
46+
"""
47+
48+
log_level = logging.WARNING
49+
log_file = cloud_config.config.get('log_file', None)
50+
if log_file:
51+
# setup the logging level
52+
get_log_level = cloud_config.config.get('log_level')
53+
if get_log_level:
54+
log_level = {
55+
'error': logging.ERROR,
56+
'info': logging.INFO,
57+
'debug': logging.DEBUG,
58+
}.get(get_log_level, logging.WARNING)
59+
60+
# setup the logging context
61+
log_cont = _LogContext(
62+
clouds_name=cloud_config.config.get('cloud'),
63+
project_name=cloud_config.auth.get('project_name'),
64+
username=cloud_config.auth.get('username'),
65+
)
66+
# setup the logging handler
67+
log_handler = _setup_handler_for_logging(
68+
logging.FileHandler,
69+
log_level,
70+
file_name=log_file,
71+
context=log_cont,
72+
)
73+
if log_level == logging.DEBUG:
74+
# DEBUG only.
75+
# setup the operation_log
76+
shell.enable_operation_logging = True
77+
shell.operation_log.setLevel(logging.DEBUG)
78+
shell.operation_log.addHandler(log_handler)
79+
80+
81+
def _setup_handler_for_logging(handler_type, level, file_name, context):
82+
"""Setup of the handler
83+
84+
Setup of the handler for addition of the logging handler,
85+
changes of the logging format, change of the logging level,
86+
87+
:param handler_type: type of logging handler
88+
:param level: logging level
89+
:param file_name: name of log-file
90+
:param context: instance of _LogContext()
91+
:return: logging handler
92+
"""
93+
94+
root_logger = logging.getLogger('')
95+
handler = None
96+
# Setup handler for FileHandler(--os-cloud)
97+
handler = logging.FileHandler(
98+
filename=file_name,
99+
)
100+
formatter = _LogContextFormatter(
101+
context=context,
102+
fmt=_LOG_MESSAGE_FORMAT,
103+
datefmt=_LOG_DATE_FORMAT,
104+
)
105+
handler.setFormatter(formatter)
106+
handler.setLevel(level)
107+
108+
# If both `--log-file` and `--os-cloud` are specified,
109+
# the log is output to each file.
110+
root_logger.addHandler(handler)
111+
112+
return handler
113+
114+
115+
class _LogContext(object):
116+
"""Helper class to represent useful information about a logging context"""
117+
118+
def __init__(self, clouds_name=None, project_name=None, username=None):
119+
"""Initialize _LogContext instance
120+
121+
:param clouds_name: one of the cloud name in configuration file
122+
:param project_name: the project name in cloud(clouds_name)
123+
:param username: the user name in cloud(clouds_name)
124+
"""
125+
126+
self.clouds_name = clouds_name
127+
self.project_name = project_name
128+
self.username = username
129+
130+
def to_dict(self):
131+
return {
132+
'clouds_name': self.clouds_name,
133+
'project_name': self.project_name,
134+
'username': self.username
135+
}
136+
137+
138+
class _LogContextFormatter(logging.Formatter):
139+
"""Customize the logging format for logging handler"""
140+
141+
def __init__(self, *args, **kwargs):
142+
self.context = kwargs.pop('context', None)
143+
logging.Formatter.__init__(self, *args, **kwargs)
144+
145+
def format(self, record):
146+
d = self.context.to_dict()
147+
for k, v in d.items():
148+
setattr(record, k, v)
149+
return logging.Formatter.format(self, record)

‎openstackclient/shell.py‎

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import openstackclient
3131
from openstackclient.common import clientmanager
3232
from openstackclient.common import commandmanager
33+
from openstackclient.common import context
3334
from openstackclient.common import exceptions as exc
3435
from openstackclient.common import timing
3536
from openstackclient.common import utils
@@ -95,6 +96,10 @@ def __init__(self):
9596

9697
self.client_manager = None
9798

99+
# Operation log
100+
self.enable_operation_logging = False
101+
self.command_options = None
102+
98103
def configure_logging(self):
99104
"""Configure logging for the app
100105
@@ -107,24 +112,30 @@ def configure_logging(self):
107112
self.options.verbose_level = 3
108113

109114
super(OpenStackShell, self).configure_logging()
110-
root_logger = logging.getLogger('')
111115

112116
# Set logging to the requested level
113117
if self.options.verbose_level == 0:
114118
# --quiet
115-
root_logger.setLevel(logging.ERROR)
119+
log_level = logging.ERROR
116120
warnings.simplefilter("ignore")
117121
elif self.options.verbose_level == 1:
118122
# This is the default case, no --debug, --verbose or --quiet
119-
root_logger.setLevel(logging.WARNING)
123+
log_level = logging.WARNING
120124
warnings.simplefilter("ignore")
121125
elif self.options.verbose_level == 2:
122126
# One --verbose
123-
root_logger.setLevel(logging.INFO)
127+
log_level = logging.INFO
124128
warnings.simplefilter("once")
125129
elif self.options.verbose_level >= 3:
126130
# Two or more --verbose
127-
root_logger.setLevel(logging.DEBUG)
131+
log_level = logging.DEBUG
132+
133+
# Set the handler logging level of FileHandler(--log-file)
134+
# and StreamHandler
135+
if self.options.log_file:
136+
context.setup_handler_logging_level(logging.FileHandler, log_level)
137+
138+
context.setup_handler_logging_level(logging.StreamHandler, log_level)
128139

129140
# Requests logs some stuff at INFO that we don't want
130141
# unless we have DEBUG
@@ -147,17 +158,31 @@ def configure_logging(self):
147158
stevedore_log.setLevel(logging.ERROR)
148159
iso8601_log.setLevel(logging.ERROR)
149160

161+
# Operation logging
162+
self.operation_log = logging.getLogger("operation_log")
163+
self.operation_log.setLevel(logging.ERROR)
164+
self.operation_log.propagate = False
165+
150166
def run(self, argv):
167+
ret_val = 1
168+
self.command_options = argv
151169
try:
152-
return super(OpenStackShell, self).run(argv)
170+
ret_val = super(OpenStackShell, self).run(argv)
171+
return ret_val
153172
except Exception as e:
154173
if not logging.getLogger('').handlers:
155174
logging.basicConfig()
156175
if self.dump_stack_trace:
157176
self.log.error(traceback.format_exc(e))
158177
else:
159178
self.log.error('Exception raised: ' + str(e))
160-
return 1
179+
if self.enable_operation_logging:
180+
self.operation_log.error(traceback.format_exc(e))
181+
182+
return ret_val
183+
184+
finally:
185+
self.log.info("END return value: %s", ret_val)
161186

162187
def build_option_parser(self, description, version):
163188
parser = super(OpenStackShell, self).build_option_parser(
@@ -243,7 +268,6 @@ def initialize_app(self, argv):
243268
auth_type = 'token_endpoint'
244269
else:
245270
auth_type = 'osc_password'
246-
self.log.debug("options: %s", self.options)
247271

248272
project_id = getattr(self.options, 'project_id', None)
249273
project_name = getattr(self.options, 'project_name', None)
@@ -266,14 +290,23 @@ def initialize_app(self, argv):
266290
# Ignore the default value of interface. Only if it is set later
267291
# will it be used.
268292
cc = cloud_config.OpenStackConfig(
269-
override_defaults={'interface': None,
270-
'auth_type': auth_type, })
271-
self.log.debug("defaults: %s", cc.defaults)
293+
override_defaults={
294+
'interface': None,
295+
'auth_type': auth_type,
296+
},
297+
)
272298

273299
self.cloud = cc.get_one_cloud(
274300
cloud=self.options.cloud,
275301
argparse=self.options,
276302
)
303+
304+
# Set up every time record log in file and logging start
305+
context.setup_logging(self, self.cloud)
306+
307+
self.log.info("START with options: %s", self.command_options)
308+
self.log.debug("options: %s", self.options)
309+
self.log.debug("defaults: %s", cc.defaults)
277310
self.log.debug("cloud cfg: %s", self.cloud.config)
278311

279312
# Set up client TLS

0 commit comments

Comments
 (0)