Skip to content

Commit e23dd6d

Browse files
Daisuke FujitaTerryHowe
authored andcommitted
Set up every time record log in file
This will allow users to record logs of all their commands into a predefined log file, in clouds.yaml. The log should have a format similar to that of oslo.log. Change-Id: I1b334bf429d575fc25809c9706fc0b11116be3f1 Implements: blueprint every-time-record-log-in-file
1 parent 46cc7d1 commit e23dd6d

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)