Integrate OSprofiler with Zaqar

*) Add osprofiler wsgi middleware
This middleware is used for 2 things:
1) It checks that person who want to trace is trusted and knows
secret HMAC key.
2) It start tracing in case of proper trace headers
and add first wsgi trace point, with info about HTTP request.

*) Add initialization of osprofiler at start of server
Initialize and set an oslo.messaging based notifier instance
to osprofiler which be used to send notifications to Ceilometer.

*) Enable profile on existing useful storage backends
Change controller creation logic of data and control panel for
mongodb, redis and sqlalchemy storage backends, as well as
an aggregative pooling driver.

*) Add options to allow operator control profiles separately

NOTE to test this:
1) You have to enable necessary profiler option(s) base on your needed.
2) You need to enable follow services in localrc for devstack:
  CEILOMETER_NOTIFICATION_TOPICS=notifications,profiler
  ENABLED_SERVICES+=,ceilometer-acompute,ceilometer-acentral
  ENABLED_SERVICES+=,ceilometer-anotification,ceilometer-collector
  ENABLED_SERVICES+=,ceilometer-alarm-evaluator,ceilometer-alarm-notifier
  ENABLED_SERVICES+=,ceilometer-api
3) You should use python-zaqarclient with this change:
  I880c003511e9e4ef99806ba5b19d0ef6996be80b

Run any command with --os-profile <SECRET_KEY>
  $ openstack --os-profile <SECRET_KEY> queue list
  # it will print <Trace ID>
Get pretty HTML with traces:
  $ osprofiler trace show --html <Trace ID>
note that osprofiler should be run from admin user name & tenant.

DocImpact

Partially-implements BP: osprofiler
Change-Id: I32565de6c447cd5e95a0ef54a9fbd4e571c2d820
Co-Authored-By: wangxiyuan <wangxiyuan@huawei.com>
This commit is contained in:
Zhi Yan Liu 2014-12-12 20:50:15 +08:00 committed by wangxiyuan
parent cd3adce177
commit 47324171d4
15 changed files with 330 additions and 39 deletions

View File

@ -13,3 +13,5 @@ namespace = zaqar.transport.base
namespace = zaqar.transport.validation
namespace = keystonemiddleware.auth_token
namespace = oslo.cache
namespace = oslo.messaging
namespace = osprofiler

View File

@ -18,9 +18,11 @@ oslo.config!=3.18.0,>=3.14.0 # Apache-2.0
oslo.context>=2.9.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=3.11.0 # Apache-2.0
oslo.messaging>=5.2.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0
oslo.utils>=3.18.0 # Apache-2.0
oslo.policy>=1.15.0 # Apache-2.0
osprofiler>=1.4.0 # Apache-2.0
SQLAlchemy<1.1.0,>=1.0.10 # MIT
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
trollius>=1.0 # Apache-2.0

View File

@ -30,3 +30,6 @@ os-api-ref>=1.0.0 # Apache-2.0
# Tempest
tempest>=12.1.0 # Apache-2.0
#OSprofiler
osprofiler>=1.4.0 # Apache-2.0

View File

@ -13,7 +13,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import socket
from oslo_log import log
from osprofiler import opts as profiler_opts
from osprofiler import profiler
from stevedore import driver
from zaqar.api import handler
@ -22,6 +26,7 @@ from zaqar.common import configs
from zaqar.common import consts
from zaqar.common import decorators
from zaqar.common import errors
from zaqar import profile
from zaqar.storage import pipeline
from zaqar.storage import pooling
from zaqar.storage import utils as storage_utils
@ -45,6 +50,11 @@ class Bootstrap(object):
for group, opts in configs._config_options():
self.conf.register_opts(opts, group=group)
profiler_opts.set_defaults(self.conf)
# TODO(wangxiyuan): Now the OSprofiler feature in Zaqar only support
# wsgi. Websockt part will be added in the future.
profile.setup(self.conf, 'Zaqar-server', socket.gethostname())
self.driver_conf = self.conf[configs._DRIVER_GROUP]
@ -62,6 +72,9 @@ class Bootstrap(object):
LOG.debug(u'Storage pooling enabled')
storage_driver = pooling.DataDriver(self.conf, self.cache,
self.control)
if self.conf.profiler.enabled:
storage_driver = profiler.trace_cls("pooling_data_"
"driver")(storage_driver)
else:
storage_driver = storage_utils.load_storage_driver(
self.conf, self.cache, control_driver=self.control)

View File

@ -71,9 +71,22 @@ _NOTIFICATION_OPTIONS = (
_NOTIFICATION_GROUP = 'notification'
_PROFILER_OPTIONS = [
cfg.BoolOpt("trace_wsgi_transport", default=False,
help="If False doesn't trace any transport requests."
"Please note that it doesn't work for websocket now."),
cfg.BoolOpt("trace_message_store", default=False,
help="If False doesn't trace any message store requests."),
cfg.BoolOpt("trace_management_store", default=False,
help="If False doesn't trace any management store requests.")
]
_PROFILER_GROUP = "profiler"
def _config_options():
return [(None, _GENERAL_OPTIONS),
(_DRIVER_GROUP, _DRIVER_OPTIONS),
(_SIGNED_URL_GROUP, _SIGNED_URL_OPTIONS),
(_NOTIFICATION_GROUP, _NOTIFICATION_OPTIONS)]
(_NOTIFICATION_GROUP, _NOTIFICATION_OPTIONS),
(_PROFILER_GROUP, _PROFILER_OPTIONS)]

116
zaqar/profile.py Normal file
View File

@ -0,0 +1,116 @@
# Copyright 2016 OpenStack, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import six
import six.moves.urllib.parse as urlparse
import webob
from oslo_log import log
from osprofiler import _utils as utils
from osprofiler import notifier
from osprofiler import profiler
from osprofiler import web
from zaqar.i18n import _LW
LOG = log.getLogger(__name__)
def setup(conf, binary, host):
if conf.profiler.enabled:
# Note(wangxiyuan): OSprofiler now support some kind of backends, such
# as Ceilometer, ElasticSearch, Messaging and MongoDB.
# 1. Ceilometer is only used for data collection, and Messaging is only
# used for data transfer. So Ceilometer only works when Messaging is
# enabled.
# 2. ElasticSearch and MongoDB support both data collection and
# transfer. So they can be used standalone.
# 3. Choose which backend depends on the config option
# "connection_string" , and the default value is "messaging://".
backend_uri = conf.profiler.connection_string
if "://" not in backend_uri:
backend_uri += "://"
parsed_connection = urlparse.urlparse(backend_uri)
backend_type = parsed_connection.scheme
if backend_type == "messaging":
import oslo_messaging
_notifier = notifier.create(backend_uri, oslo_messaging, {},
oslo_messaging.get_transport(conf),
"Zaqar", binary, host)
else:
_notifier = notifier.create(backend_uri, project="Zaqar",
service=binary, host=host)
notifier.set(_notifier)
LOG.warning(_LW("OSProfiler is enabled.\nIt means that person who "
"knows any of hmac_keys that are specified in "
"/etc/zaqar/zaqar.conf can trace his requests. \n In "
"real life only operator can read this file so there "
"is no security issue. Note that even if person can "
"trigger profiler, only admin user can retrieve trace "
"information.\n"
"To disable OSprofiler set in zaqar.conf:\n"
"[profiler]\nenabled=false"))
web.enable(conf.profiler.hmac_keys)
else:
web.disable()
class ProfileWSGIMiddleware(object):
def __init__(self, application, hmac_keys=None, enabled=False):
self.application = application
self.name = "wsgi"
self.enabled = enabled
self.hmac_keys = utils.split(hmac_keys or "")
def _trace_is_valid(self, trace_info):
if not isinstance(trace_info, dict):
return False
trace_keys = set(six.iterkeys(trace_info))
if not all(k in trace_keys for k in web._REQUIRED_KEYS):
return False
if trace_keys.difference(web._REQUIRED_KEYS + web._OPTIONAL_KEYS):
return False
return True
def __call__(self, environ, start_response):
request = webob.Request(environ)
trace_info = utils.signed_unpack(request.headers.get(web.X_TRACE_INFO),
request.headers.get(web.X_TRACE_HMAC),
self.hmac_keys)
if not self._trace_is_valid(trace_info):
return self.application(environ, start_response)
profiler.init(**trace_info)
info = {
"request": {
"path": request.path,
"query": request.query_string,
"method": request.method,
"scheme": request.scheme
}
}
with profiler.Trace(self.name, info=info):
return self.application(environ, start_response)
def install_wsgi_tracer(app, conf):
enabled = conf.profiler.enabled and conf.profiler.trace_wsgi_transport
if enabled:
LOG.debug(u'Installing osprofiler\'s wsgi tracer')
return ProfileWSGIMiddleware(app, conf.profiler.hmac_keys, enabled=enabled)

View File

@ -17,6 +17,7 @@
import ssl
from osprofiler import profiler
import pymongo
import pymongo.errors
@ -184,15 +185,31 @@ class DataDriver(storage.DataDriverBase):
@decorators.lazy_property(write=False)
def message_controller(self):
return controllers.MessageController(self)
controller = controllers.MessageController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_message_store):
return profiler.trace_cls("mongodb_message_controller")(controller)
else:
return controller
@decorators.lazy_property(write=False)
def claim_controller(self):
return controllers.ClaimController(self)
controller = controllers.ClaimController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_message_store):
return profiler.trace_cls("mongodb_claim_controller")(controller)
else:
return controller
@decorators.lazy_property(write=False)
def subscription_controller(self):
return controllers.SubscriptionController(self)
controller = controllers.SubscriptionController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_message_store):
return profiler.trace_cls("mongodb_subscription_"
"controller")(controller)
else:
return controller
class FIFODataDriver(DataDriver):
@ -206,7 +223,12 @@ class FIFODataDriver(DataDriver):
@decorators.lazy_property(write=False)
def message_controller(self):
return controllers.FIFOMessageController(self)
controller = controllers.FIFOMessageController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_message_store):
return profiler.trace_cls("mongodb_message_controller")(controller)
else:
return controller
class ControlDriver(storage.ControlDriverBase):
@ -245,16 +267,38 @@ class ControlDriver(storage.ControlDriverBase):
@decorators.lazy_property(write=False)
def queue_controller(self):
return controllers.QueueController(self)
controller = controllers.QueueController(self)
if (self.conf.profiler.enabled and
(self.conf.profiler.trace_message_store or
self.conf.profiler.trace_management_store)):
return profiler.trace_cls("mongodb_queues_controller")(controller)
else:
return controller
@property
def pools_controller(self):
return controllers.PoolsController(self)
controller = controllers.PoolsController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_management_store):
return profiler.trace_cls("mongodb_pools_controller")(controller)
else:
return controller
@property
def catalogue_controller(self):
return controllers.CatalogueController(self)
controller = controllers.CatalogueController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_management_store):
return profiler.trace_cls("mongodb_catalogue_"
"controller")(controller)
else:
return controller
@property
def flavors_controller(self):
return controllers.FlavorsController(self)
controller = controllers.FlavorsController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_management_store):
return profiler.trace_cls("mongodb_flavors_controller")(controller)
else:
return controller

View File

@ -14,6 +14,7 @@
from oslo_config import cfg
from oslo_log import log as logging
from osprofiler import profiler
from stevedore import driver
from stevedore import extension
@ -85,7 +86,7 @@ def _get_storage_pipeline(resource_name, conf, *args, **kwargs):
return pipeline
def _get_builtin_entry_points(resource_name, storage, control_driver):
def _get_builtin_entry_points(resource_name, storage, control_driver, conf):
# Load builtin stages
builtin_entry_points = []
@ -104,7 +105,9 @@ def _get_builtin_entry_points(resource_name, storage, control_driver):
for ext in extensions.extensions:
builtin_entry_points.append(ext.obj)
if conf.profiler.enabled and conf.profiler.trace_message_store:
return (profiler.trace_cls("stages_controller")
(builtin_entry_points))
return builtin_entry_points
@ -138,7 +141,7 @@ class DataDriver(base.DataDriverBase):
@decorators.lazy_property(write=False)
def queue_controller(self):
stages = _get_builtin_entry_points('queue', self._storage,
self.control_driver)
self.control_driver, self.conf)
stages.extend(_get_storage_pipeline('queue', self.conf))
stages.append(self._storage.queue_controller)
return common.Pipeline(stages)
@ -146,7 +149,7 @@ class DataDriver(base.DataDriverBase):
@decorators.lazy_property(write=False)
def message_controller(self):
stages = _get_builtin_entry_points('message', self._storage,
self.control_driver)
self.control_driver, self.conf)
kwargs = {'subscription_controller':
self._storage.subscription_controller,
'max_notifier_workers':
@ -160,7 +163,7 @@ class DataDriver(base.DataDriverBase):
@decorators.lazy_property(write=False)
def claim_controller(self):
stages = _get_builtin_entry_points('claim', self._storage,
self.control_driver)
self.control_driver, self.conf)
stages.extend(_get_storage_pipeline('claim', self.conf))
stages.append(self._storage.claim_controller)
return common.Pipeline(stages)
@ -168,7 +171,7 @@ class DataDriver(base.DataDriverBase):
@decorators.lazy_property(write=False)
def subscription_controller(self):
stages = _get_builtin_entry_points('subscription', self._storage,
self.control_driver)
self.control_driver, self.conf)
stages.extend(_get_storage_pipeline('subscription', self.conf))
stages.append(self._storage.subscription_controller)
return common.Pipeline(stages)

View File

@ -18,6 +18,7 @@ import itertools
from oslo_config import cfg
from oslo_log import log
from osprofiler import profiler
from zaqar.common import decorators
from zaqar.common import errors as cerrors
@ -74,7 +75,11 @@ class DataDriver(storage.DataDriverBase):
def __init__(self, conf, cache, control, control_driver=None):
super(DataDriver, self).__init__(conf, cache, control_driver)
self._pool_catalog = Catalog(conf, cache, control)
catalog = Catalog(conf, cache, control)
if self.conf.profiler.enabled:
catalog = profiler.trace_cls("pooling_catalogue_"
"controller")(catalog)
self._pool_catalog = catalog
@property
def capabilities(self):
@ -121,19 +126,36 @@ class DataDriver(storage.DataDriverBase):
@decorators.lazy_property(write=False)
def queue_controller(self):
return QueueController(self._pool_catalog)
controller = QueueController(self._pool_catalog)
if self.conf.profiler.enabled:
return profiler.trace_cls("pooling_queue_controller")(controller)
else:
return controller
@decorators.lazy_property(write=False)
def message_controller(self):
return MessageController(self._pool_catalog)
controller = MessageController(self._pool_catalog)
if self.conf.profiler.enabled:
return profiler.trace_cls("pooling_message_controller")(controller)
else:
return controller
@decorators.lazy_property(write=False)
def claim_controller(self):
return ClaimController(self._pool_catalog)
controller = ClaimController(self._pool_catalog)
if self.conf.profiler.enabled:
return profiler.trace_cls("pooling_claim_controller")(controller)
else:
return controller
@decorators.lazy_property(write=False)
def subscription_controller(self):
return SubscriptionController(self._pool_catalog)
controller = SubscriptionController(self._pool_catalog)
if self.conf.profiler.enabled:
return (profiler.trace_cls("pooling_subscription_controller")
(controller))
else:
return controller
class QueueController(storage.Queue):

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from osprofiler import profiler
import redis
import redis.sentinel
from six.moves import urllib
@ -198,15 +199,31 @@ class DataDriver(storage.DataDriverBase):
@decorators.lazy_property(write=False)
def message_controller(self):
return controllers.MessageController(self)
controller = controllers.MessageController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_message_store):
return profiler.trace_cls("redis_message_controller")(controller)
else:
return controller
@decorators.lazy_property(write=False)
def claim_controller(self):
return controllers.ClaimController(self)
controller = controllers.ClaimController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_message_store):
return profiler.trace_cls("redis_claim_controller")(controller)
else:
return controller
@decorators.lazy_property(write=False)
def subscription_controller(self):
return controllers.SubscriptionController(self)
controller = controllers.SubscriptionController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_message_store):
return profiler.trace_cls("mongodb_subscription_"
"controller")(controller)
else:
return controller
class ControlDriver(storage.ControlDriverBase):
@ -229,7 +246,13 @@ class ControlDriver(storage.ControlDriverBase):
@decorators.lazy_property(write=False)
def queue_controller(self):
return controllers.QueueController(self)
controller = controllers.QueueController(self)
if (self.conf.profiler.enabled and
(self.conf.profiler.trace_message_store or
self.conf.profiler.trace_management_store)):
return profiler.trace_cls("redis_queue_controller")(controller)
else:
return controller
@property
def pools_controller(self):

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations under
# the License.
from osprofiler import profiler
from osprofiler import sqlalchemy as sa_tracer
import sqlalchemy as sa
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker
@ -60,6 +62,11 @@ class ControlDriver(storage.ControlDriverBase):
self._mysql_on_connect)
tables.metadata.create_all(engine, checkfirst=True)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_message_store):
sa_tracer.add_tracing(sa, engine, "db")
return engine
# TODO(cpp-cabrera): expose connect/close as a context manager
@ -81,20 +88,41 @@ class ControlDriver(storage.ControlDriverBase):
@property
def pools_controller(self):
return controllers.PoolsController(self)
controller = controllers.PoolsController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_management_store):
return profiler.trace_cls("sqlalchemy_pools_"
"controller")(controller)
else:
return controller
@property
def queue_controller(self):
return controllers.QueueController(self)
controller = controllers.QueueController(self)
if (self.conf.profiler.enabled and
(self.conf.profiler.trace_message_store or
self.conf.profiler.trace_management_store)):
return profiler.trace_cls("sqlalchemy_queue_"
"controller")(controller)
else:
return controller
@property
def catalogue_controller(self):
return controllers.CatalogueController(self)
controller = controllers.CatalogueController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_management_store):
return profiler.trace_cls("sqlalchemy_catalogue_"
"controller")(controller)
else:
return controller
@property
def flavors_controller(self):
return controllers.FlavorsController(self)
@property
def subscriptions_controller(self):
pass
controller = controllers.FlavorsController(self)
if (self.conf.profiler.enabled and
self.conf.profiler.trace_management_store):
return profiler.trace_cls("sqlalchemy_flavors_"
"controller")(controller)
else:
return controller

View File

@ -16,6 +16,7 @@ import copy
from oslo_config import cfg
from oslo_log import log
from osprofiler import profiler
import six
from stevedore import driver
@ -139,7 +140,14 @@ def load_storage_driver(conf, cache, storage_type=None,
invoke_on_load=True,
invoke_args=_invoke_args)
return mgr.driver
if conf.profiler.enabled:
if ((mode == "control" and conf.profiler.trace_management_store) or
(mode == "data" and conf.profiler.trace_message_store)):
trace_name = '{0}_{1}_driver'.format(storage_type, mode)
return profiler.trace_cls(trace_name,
trace_private=True)(mgr.driver)
else:
return mgr.driver
except Exception as exc:
LOG.error(_LE('Failed to load "{}" driver for "{}"').format(

View File

@ -18,6 +18,7 @@ import os
import fixtures
from oslo_config import cfg
from oslo_log import log
from osprofiler import opts
import testtools
from zaqar.common import configs
@ -62,6 +63,9 @@ class TestBase(testtools.TestCase):
group=configs._NOTIFICATION_GROUP)
self.conf.register_opts(configs._SIGNED_URL_OPTIONS,
group=configs._SIGNED_URL_GROUP)
opts.set_defaults(self.conf)
self.conf.register_opts(configs._PROFILER_OPTIONS,
group=configs._PROFILER_GROUP)
self.mongodb_url = os.environ.get('ZAQAR_TEST_MONGODB_URL',
'mongodb://127.0.0.1:27017')

View File

@ -35,11 +35,14 @@ class TestUtils(testing.TestBase):
@testing.requires_redis
def test_can_connect_succeeds_if_good_uri_redis(self):
self.assertTrue(utils.can_connect('redis://localhost'))
self.assertTrue(utils.can_connect('redis://localhost:6379'))
self.assertTrue(utils.can_connect('redis://localhost',
conf=self.conf))
self.assertTrue(utils.can_connect('redis://localhost:6379',
conf=self.conf))
def test_can_connect_fails_if_bad_uri_missing_schema(self):
self.assertFalse(utils.can_connect('localhost:27017'))
self.assertFalse(utils.can_connect('localhost:27017',
conf=self.conf))
@testing.requires_mongodb
def test_can_connect_fails_if_bad_uri_mongodb(self):
@ -53,5 +56,7 @@ class TestUtils(testing.TestBase):
@testing.requires_redis
def test_can_connect_fails_if_bad_uri_redis(self):
self.assertFalse(utils.can_connect('redis://localhost:8080'))
self.assertFalse(utils.can_connect('redis://example.com:6379'))
self.assertFalse(utils.can_connect('redis://localhost:8080',
conf=self.conf))
self.assertFalse(utils.can_connect('redis://example.com:6379',
conf=self.conf))

View File

@ -26,6 +26,7 @@ import socket
from zaqar.common import decorators
from zaqar.common.transport.wsgi import helpers
from zaqar.i18n import _
from zaqar import profile
from zaqar import transport
from zaqar.transport import acl
from zaqar.transport import auth
@ -138,8 +139,12 @@ class Driver(transport.DriverBase):
def _init_middleware(self):
"""Initialize WSGI middlewarez."""
auth_app = self.app
# NOTE(zhiyan): Install Profiler
if (self._conf.profiler.enabled and
self._conf.profiler.trace_wsgi_transport):
self.app = profile.install_wsgi_tracer(self.app, self._conf)
auth_app = self.app
# NOTE(flaper87): Install Auth
if self._conf.auth_strategy:
strategy = auth.strategy(self._conf.auth_strategy)