예제 #1
0
    def __init__(self, args=None, config_path=None):
        logging.Handler.__init__(self)
        if args is not None and args != '':
            config_path = args
        if not os.path.isfile(config_path):
            raise Exception('Invalid path to config file.')
        with open(config_path) as config_file_obj:
            self.config = load(config_file_obj.read())
        for key in self.DEFAULT_CONFIG:
            setattr(
                self, key,
                self.config.get('main', {}).get(key, None)
                or self.DEFAULT_CONFIG[key])

        # Initialize Statsd Client
        self.statsd = DogStatsd(host=self.host,
                                port=self.port,
                                namespace=self.app_key)

        self.publish_templates = self.DEFAULT_PUBLISH_TEMPLATES
        publish_templates = self.config.get('publish_templates', {})
        self.publish_templates.update(publish_templates)
        self.counters = self.config.get('counters', {})
        self.gauges = self.config.get('gauges', {})
        self.timers = self.config.get('timers', [])
        self.histograms = self.config.get('histograms', {})
        self.sets = self.config.get('sets', {})
        self.timers_start_keys = self._get_timers_keys_list('start')
        self.timers_end_keys = self._get_timers_keys_list('end')
        self.timers_value_keys = self._get_timers_keys_list('value')
예제 #2
0
    def udf(df: pd.DataFrame) -> pd.Series:
        from datadog.dogstatsd import DogStatsd

        reporter = (DogStatsd(
            host=os.environ["STATSD_HOST"],
            port=int(os.environ["STATSD_PORT"]),
            telemetry_min_flush_interval=0,
        ) if os.getenv("STATSD_HOST") and os.getenv("STATSD_PORT") else
                    DogStatsd())

        ds = PandasDataset.from_dataset(df)
        result = ds.validate(expectations, result_format="COMPLETE")
        valid_rows = pd.Series([True] * df.shape[0])

        for check in result.results:
            if check.exception_info["raised_exception"]:
                # ToDo: probably we should mark all rows as invalid
                continue

            check_kwargs = check.expectation_config.kwargs
            check_kwargs.pop("result_format", None)
            check_name = "_".join([check.expectation_config.expectation_type] +
                                  [
                                      str(v) for v in check_kwargs.values()
                                      if isinstance(v, (str, int, float))
                                  ])

            if ("unexpected_count" in check.result
                    and check.result["unexpected_count"] > 0):
                reporter.increment(
                    "feast_feature_validation_check_failed",
                    value=check.result["unexpected_count"],
                    tags=[
                        f"feature_table:{os.getenv('FEAST_INGESTION_FEATURE_TABLE', 'unknown')}",
                        f"project:{os.getenv('FEAST_INGESTION_PROJECT_NAME', 'default')}",
                        f"check:{check_name}",
                    ],
                )

                valid_rows.iloc[check.result["unexpected_index_list"]] = False

            elif "observed_value" in check.result and check.result[
                    "observed_value"]:
                reporter.gauge(
                    "feast_feature_validation_observed_value",
                    value=int(check.result["observed_value"] *
                              100  # storing as decimal with precision 2
                              ) if not check.success else
                    0,  # nullify everything below threshold
                    tags=[
                        f"feature_table:{os.getenv('FEAST_INGESTION_FEATURE_TABLE', 'unknown')}",
                        f"project:{os.getenv('FEAST_INGESTION_PROJECT_NAME', 'default')}",
                        f"check:{check_name}",
                    ],
                )

        return valid_rows
    def __init__(self,
                 host='localhost',
                 port=8125,
                 prefix=None,
                 maxudpsize=512,
                 ipv6=False):
        if ipv6:
            _log.warning("DogStatsdAdapter() was 'ipv6'. This is ignored")

        self._dd_client = DogStatsd(host=host,
                                    port=port,
                                    namespace=prefix,
                                    max_buffer_size=maxudpsize)
예제 #4
0
 def __init__(self,
              host: str = 'localhost',
              port: int = 8125,
              prefix: str = 'faust-app',
              rate: float = 1.0,
              **kwargs: Any) -> None:
     self.client = DogStatsd(host=host,
                             port=port,
                             namespace=prefix,
                             **kwargs)
     self.rate = rate
     self.sanitize_re = re.compile(r'[^0-9a-zA-Z_]')
     self.re_substitution = '_'
예제 #5
0
 def __init__(self,
              app,
              statsd_host='localhost',
              statsd_port='8125',
              statsd_prefix='openstack',
              statsd_replace='id'):
     self.app = app
     self.replace_strategy = _ReplaceStrategy(
         os.getenv('STATSD_REPLACE', statsd_replace))
     self.client = DogStatsd(host=os.getenv('STATSD_HOST', statsd_host),
                             port=int(os.getenv('STATSD_PORT',
                                                statsd_port)),
                             namespace=os.getenv('STATSD_PREFIX',
                                                 statsd_prefix))
예제 #6
0
class DogStatsdMonitoring(AbstractMonitoring):
    def __init__(self, config):
        super().__init__(config)
        self.client = DogStatsd()

    def send(self, tags, value):
        if len(tags) != 3:
            raise AssertionError(
                "Datadog monitoring implementation needs 3 tags: 'name', 'what' and 'backup_name'"
            )

        name, what, backup_name = tags
        metric = '{name}.{what}'.format(name=name, what=what)
        backup_name_tag = 'backup_name:{}'.format(backup_name)

        # The backup_name  would be a rather high cardinality metrics series if backups are at all frequent.
        # This could be a expensive metric so backup_name is droppped from the tags sent by default
        if medusa.utils.evaluate_boolean(self.config.send_backup_name_tag):
            self.client.gauge(metric, value, tags=[backup_name_tag])
        else:
            self.client.gauge(metric, value)
예제 #7
0
    def __init__(
        self,
        host: str = 'localhost',
        port: int = 8125,
        socket_path: str = None,
        prefix: str = None,
        default_tags: Dict[str, Any] = None,
    ):
        """
        This is a wrapper that does primarily this:

        - setup connection to statsd server
        - wrap measuring methods such that they can be used as various things
            (context managers, decorators)
        -


        :param host: Host of the statsd server
        :param port: Port of the statsd server
        :param prefix: Default prefix to add to all metrics
        :param default_tags: Default tags to add to all metrics
        """
        # Setup stats connection
        self._statsd = DogStatsd(
            host=host,
            port=port,
            socket_path=socket_path,
            constant_tags=_dict_as_statsd_tags(default_tags))

        # Add measurement methods
        self.increment = self._wrap_measurement_method(
            self._statsd.increment,
            default_value=1,
            prefix=self._join_with_prefix(config.measurement.PREFIX_COUNTER,
                                          prefix),
        )
        self.decrement = self._wrap_measurement_method(
            self._statsd.decrement,
            default_value=1,
            prefix=self._join_with_prefix(config.measurement.PREFIX_COUNTER,
                                          prefix),
        )
        self.gauge = self._wrap_measurement_method(
            self._statsd.gauge,
            prefix=self._join_with_prefix(config.measurement.PREFIX_GAUGE,
                                          prefix))

        self.histogram = self._wrap_measurement_method(
            self._statsd.histogram,
            prefix=self._join_with_prefix(config.measurement.PREFIX_HISTOGRAM,
                                          prefix))

        self.timing = self._wrap_measurement_method(
            self._statsd.timing,
            prefix=self._join_with_prefix(config.measurement.PREFIX_TIMING,
                                          prefix))
        self.set = self._wrap_measurement_method(
            self._statsd.set,
            prefix=self._join_with_prefix(config.measurement.PREFIX_SET,
                                          prefix))

        # Our own augmented measurement primitives
        self.timer = self._wrap_measurement_method(
            self._statsd.timing,
            prefix=self._join_with_prefix(config.measurement.PREFIX_TIMING,
                                          prefix),
            wrapper=TimerMeasuringPrimitive,
        )

        self.counter = self._wrap_measurement_method(
            self._statsd.increment,
            prefix=self._join_with_prefix(config.measurement.PREFIX_COUNTER,
                                          prefix),
            wrapper=CounterMeasuringPrimitive,
        )
예제 #8
0
class MeasureWrapper:
    """
    Wraps connection to the statsd server and creates wrapped measuring methods
    on top of the class.

    Provided you create an instance Measure = MeasureWrapper(*args), you can
    then use it like this:

    Measure.increment('metric')(value)

    or with context managers:

    with Measure.increment('metric') as m:
        m(value)

    or with function decorators

    @Measure.increment('metric')
    def my_method(blah, measuring_context):
        measuring_context(value)
    """

    _statsd = None

    # Statsd primitives
    decrement = None  # type: MeasuringPrimitive
    gauge = None  # type: MeasuringPrimitive
    histogram = None  # type: MeasuringPrimitive
    increment = None  # type: MeasuringPrimitive
    set = None  # type: MeasuringPrimitive
    timing = None  # type: MeasuringPrimitive

    # Datadogs primitives
    # TODO: include them too? Will be easy

    # Our own little bit more interesting measuring primitives
    counter = None  # type: CounterMeasuringPrimitive
    timer = None  # type: TimerMeasuringPrimitive

    def __init__(
        self,
        host: str = 'localhost',
        port: int = 8125,
        socket_path: str = None,
        prefix: str = None,
        default_tags: Dict[str, Any] = None,
    ):
        """
        This is a wrapper that does primarily this:

        - setup connection to statsd server
        - wrap measuring methods such that they can be used as various things
            (context managers, decorators)
        -


        :param host: Host of the statsd server
        :param port: Port of the statsd server
        :param prefix: Default prefix to add to all metrics
        :param default_tags: Default tags to add to all metrics
        """
        # Setup stats connection
        self._statsd = DogStatsd(
            host=host,
            port=port,
            socket_path=socket_path,
            constant_tags=_dict_as_statsd_tags(default_tags))

        # Add measurement methods
        self.increment = self._wrap_measurement_method(
            self._statsd.increment,
            default_value=1,
            prefix=self._join_with_prefix(config.measurement.PREFIX_COUNTER,
                                          prefix),
        )
        self.decrement = self._wrap_measurement_method(
            self._statsd.decrement,
            default_value=1,
            prefix=self._join_with_prefix(config.measurement.PREFIX_COUNTER,
                                          prefix),
        )
        self.gauge = self._wrap_measurement_method(
            self._statsd.gauge,
            prefix=self._join_with_prefix(config.measurement.PREFIX_GAUGE,
                                          prefix))

        self.histogram = self._wrap_measurement_method(
            self._statsd.histogram,
            prefix=self._join_with_prefix(config.measurement.PREFIX_HISTOGRAM,
                                          prefix))

        self.timing = self._wrap_measurement_method(
            self._statsd.timing,
            prefix=self._join_with_prefix(config.measurement.PREFIX_TIMING,
                                          prefix))
        self.set = self._wrap_measurement_method(
            self._statsd.set,
            prefix=self._join_with_prefix(config.measurement.PREFIX_SET,
                                          prefix))

        # Our own augmented measurement primitives
        self.timer = self._wrap_measurement_method(
            self._statsd.timing,
            prefix=self._join_with_prefix(config.measurement.PREFIX_TIMING,
                                          prefix),
            wrapper=TimerMeasuringPrimitive,
        )

        self.counter = self._wrap_measurement_method(
            self._statsd.increment,
            prefix=self._join_with_prefix(config.measurement.PREFIX_COUNTER,
                                          prefix),
            wrapper=CounterMeasuringPrimitive,
        )

    def __enter__(self):
        self._statsd.__enter__()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._statsd.__exit__(exc_type, exc_val, exc_tb)

    def _wrap_measurement_method(self,
                                 func,
                                 prefix,
                                 default_value=None,
                                 wrapper=None):
        """
        We need to wrap the singular measurement function with our
        MeasuringPrimitive (or a subclass thereof) class, so that we can support
        various interfaces on top of it.

        If the measurements are disabled, we just replace the statsd function do
        nothing.

        :param function func: The function to be wrapped
        :param string prefix: Common metric prefix
        :param any default_value: Default value for the metric
        :param MeasuringPrimitive wrapper: The wrapper class to use
        :return function: The partial to be called on the MeasuringPrimitive
            constructor
        """
        return functools.partial(wrapper or MeasuringPrimitive, func, prefix,
                                 default_value)

    def _join_with_prefix(self, value_prefix, global_prefix):
        """
        Joins prefixes together, useful for combining global and metric type
        specific prefix
        """
        return '.'.join(filter(None, [global_prefix, value_prefix]))
예제 #9
0
    def __init__(self, app, config, logger=logging.getLogger(__name__)):
        self.logger = logger
        self.app = app
        self.wsgi_config = config
        self.watcher_config = {}

        self.cadf_service_name = self.wsgi_config.get('cadf_service_name',
                                                      None)
        self.service_type = self.wsgi_config.get('service_type',
                                                 taxonomy.UNKNOWN)
        # get the project uid from the request path or from the token (default)
        self.is_project_id_from_path = common.string_to_bool(
            self.wsgi_config.get('target_project_id_from_path', 'False'))
        # get the project id from the service catalog (see documentation on keystone auth_token middleware)
        self.is_project_id_from_service_catalog = common.string_to_bool(
            self.wsgi_config.get('target_project_id_from_service_catalog',
                                 'False'))

        # whether to include the target project id in the metrics
        self.is_include_target_project_id_in_metric = common.string_to_bool(
            self.wsgi_config.get('include_target_project_id_in_metric',
                                 'True'))
        # whether to include the target domain id in the metrics
        self.is_include_target_domain_id_in_metric = common.string_to_bool(
            self.wsgi_config.get('include_target_domain_id_in_metric', 'True'))
        # whether to include the initiator user id in the metrics
        self.is_include_initiator_user_id_in_metric = common.string_to_bool(
            self.wsgi_config.get('include_initiator_user_id_in_metric',
                                 'False'))

        config_file_path = config.get('config_file', None)
        if config_file_path:
            try:
                self.watcher_config = load_config(config_file_path)
            except errors.ConfigError as e:
                self.logger.debug("custom actions not available: %s", str(e))

        custom_action_config = self.watcher_config.get('custom_actions', {})
        path_keywords = self.watcher_config.get('path_keywords', {})
        keyword_exclusions = self.watcher_config.get('keyword_exclusions', {})
        regex_mapping = self.watcher_config.get('regex_path_mapping', {})

        # init the strategy used to determine the target type uri
        strat = STRATEGIES.get(self.service_type, strategies.BaseCADFStrategy)

        # set custom prefix to target type URI or use defaults
        target_type_uri_prefix = common.SERVICE_TYPE_CADF_PREFIX_MAP.get(
            self.service_type, 'service/{0}'.format(self.service_type))

        if self.cadf_service_name:
            target_type_uri_prefix = self.cadf_service_name

        strategy = strat(target_type_uri_prefix=target_type_uri_prefix,
                         path_keywords=path_keywords,
                         keyword_exclusions=keyword_exclusions,
                         custom_action_config=custom_action_config,
                         regex_mapping=regex_mapping)

        self.strategy = strategy

        self.metric_client = DogStatsd(
            host=self.wsgi_config.get("statsd_host", "127.0.0.1"),
            port=int(self.wsgi_config.get("statsd_port", 9125)),
            namespace=self.wsgi_config.get("statsd_namespace",
                                           "openstack_watcher"))
예제 #10
0
class DogStatsdMetrics(Metrics):
    def __init__(self,
                 id,
                 prefix=None,
                 tags=None,
                 host="127.0.0.1",
                 port=8125):
        self.id = id
        self.prefix = prefix
        self.tags = tags or {}
        self.host = host
        self.port = port
        self.tags["instance"] = id

    def setup(self):
        from datadog.dogstatsd import DogStatsd

        self.client = DogStatsd(host=self.host, port=self.port)

    def gauge(self, metric, value, tags=None, sample_rate=1):
        self.client.gauge(
            self._get_key(metric),
            value,
            sample_rate=sample_rate,
            tags=self._get_tags(tags),
        )

    def increment(self, metric, value=1, tags=None, sample_rate=1):
        self.client.increment(
            self._get_key(metric),
            value,
            sample_rate=sample_rate,
            tags=self._get_tags(tags),
        )

    def decrement(self, metric, value=1, tags=None, sample_rate=1):
        self.client.decrement(
            self._get_key(metric),
            value,
            sample_rate=sample_rate,
            tags=self._get_tags(tags),
        )

    def histogram(self, metric, value, tags=None, sample_rate=1):
        self.client.histogram(
            self._get_key(metric),
            value,
            sample_rate=sample_rate,
            tags=self._get_tags(tags),
        )

    def timing(self, metric, value, tags=None, sample_rate=1):
        self.client.timing(
            self._get_key(metric),
            value,
            sample_rate=sample_rate,
            tags=self._get_tags(tags),
        )

    def timed(self, metric, tags=None, sample_rate=1, use_ms=None):
        self.client.timed(
            self._get_key(metric),
            sample_rate=sample_rate,
            tags=self._get_tags(tags),
            use_ms=use_ms,
        )
예제 #11
0
class DatadogStatsClient:
    """Statsd compliant datadog client."""
    def __init__(self,
                 host: str = 'localhost',
                 port: int = 8125,
                 prefix: str = 'faust-app',
                 rate: float = 1.0,
                 **kwargs: Any) -> None:
        self.client = DogStatsd(host=host,
                                port=port,
                                namespace=prefix,
                                **kwargs)
        self.rate = rate
        self.sanitize_re = re.compile(r'[^0-9a-zA-Z_]')
        self.re_substitution = '_'

    def gauge(self, metric: str, value: float, labels: Dict = None) -> None:
        self.client.gauge(
            metric,
            value=value,
            tags=self._encode_labels(labels),
            sample_rate=self.rate,
        )

    def increment(self,
                  metric: str,
                  value: float = 1.0,
                  labels: Dict = None) -> None:
        self.client.increment(
            metric,
            value=value,
            tags=self._encode_labels(labels),
            sample_rate=self.rate,
        )

    def incr(self, metric: str, count: int = 1) -> None:
        """Statsd compatibility."""
        self.increment(metric, value=count)

    def decrement(self,
                  metric: str,
                  value: float = 1.0,
                  labels: Dict = None) -> float:
        return self.client.decrement(
            metric,
            value=value,
            tags=self._encode_labels(labels),
            sample_rate=self.rate,
        )

    def decr(self, metric: str, count: float = 1.0) -> None:
        """Statsd compatibility."""
        self.decrement(metric, value=count)

    def timing(self, metric: str, value: float, labels: Dict = None) -> None:
        self.client.timing(
            metric,
            value=value,
            tags=self._encode_labels(labels),
            sample_rate=self.rate,
        )

    def timed(self,
              metric: str = None,
              labels: Dict = None,
              use_ms: bool = None) -> float:
        return self.client.timed(
            metric=metric,
            tags=self._encode_labels(labels),
            sample_rate=self.rate,
            use_ms=use_ms,
        )

    def histogram(self,
                  metric: str,
                  value: float,
                  labels: Dict = None) -> None:
        self.client.histogram(
            metric,
            value=value,
            tags=self._encode_labels(labels),
            sample_rate=self.rate,
        )

    def _encode_labels(self, labels: Optional[Dict]) -> Optional[List[str]]:
        def sanitize(s: str) -> str:
            return self.sanitize_re.sub(self.re_substitution, str(s))

        return [f'{sanitize(k)}:{sanitize(v)}'
                for k, v in labels.items()] if labels else None
예제 #12
0
class StatsdMiddleware(object):
    def __init__(self,
                 app,
                 statsd_host='localhost',
                 statsd_port='8125',
                 statsd_prefix='openstack',
                 statsd_replace='id'):
        self.app = app
        self.replace_strategy = _ReplaceStrategy(
            os.getenv('STATSD_REPLACE', statsd_replace))
        self.client = DogStatsd(host=os.getenv('STATSD_HOST', statsd_host),
                                port=int(os.getenv('STATSD_PORT',
                                                   statsd_port)),
                                namespace=os.getenv('STATSD_PREFIX',
                                                    statsd_prefix))

    @classmethod
    def factory(cls, global_config, **local_config):
        def _factory(app):
            return cls(app, **local_config)

        return _factory

    def process_response(self,
                         start,
                         environ,
                         response_wrapper,
                         exception=None):
        self.client.increment('responses_total')

        status = response_wrapper.get('status')
        if status:
            status_code = status.split()[0]
        else:
            status_code = 'none'

        method = environ['REQUEST_METHOD']

        # cleanse request path
        path = urlparse.urlparse(environ['SCRIPT_NAME'] +
                                 environ['PATH_INFO']).path
        # strip extensions
        path = splitext(path)[0]

        # replace parts of the path with constants based on strategy
        path = self.replace_strategy.apply(path)

        parts = path.rstrip('\/').split('/')
        if exception:
            parts.append(exception.__class__.__name__)
        api = '/'.join(parts)

        self.client.timing('latency_by_api',
                           time.time() - start,
                           tags=['method:%s' % method,
                                 'api:%s' % api])

        self.client.increment('responses_by_api',
                              tags=[
                                  'method:%s' % method,
                                  'api:%s' % api,
                                  'status:%s' % status_code
                              ])

    def __call__(self, environ, start_response):
        response_interception = {}

        def start_response_wrapper(status, response_headers, exc_info=None):
            response_interception.update(status=status,
                                         response_headers=response_headers,
                                         exc_info=exc_info)
            return start_response(status, response_headers, exc_info)

        start = time.time()
        try:
            self.client.open_buffer()
            self.client.increment('requests_total')

            response = self.app(environ, start_response_wrapper)
            try:
                for event in response:
                    yield event
            finally:
                if hasattr(response, 'close'):
                    response.close()

            self.process_response(start, environ, response_interception)
        except Exception as exception:
            self.process_response(start, environ, response_interception,
                                  exception)
            raise
        finally:
            self.client.close_buffer()
예제 #13
0
def _create_statsd(*args, **kwargs):
    # testing mock point
    return DogStatsd(*args, **kwargs)
예제 #14
0
 def __init__(self, config):
     super().__init__(config)
     self.client = DogStatsd()
예제 #15
0
import os

from datadog import initialize
from datadog.dogstatsd import DogStatsd

import config

root = os.path.dirname(os.path.abspath(__file__))
conf = config.Config(os.path.join(root, "config.ini"))

dogstatsd = DogStatsd(host='localhost',
                      port=conf.datadog_dogstatsd_port,
                      constant_tags=conf.datadog_tags)
class DogStatsdAdapter(object):
    """
    A wrapper around DogStatsd that supports the full statsd.StatsClient interface
    Note that `tags` is available on all these methods, but this is not supported by statsd.StatsClient. It has been
    added, but should only be used when you are CERTAIN that you will be using this class (or something similar)
    """
    def __init__(self,
                 host='localhost',
                 port=8125,
                 prefix=None,
                 maxudpsize=512,
                 ipv6=False):
        if ipv6:
            _log.warning("DogStatsdAdapter() was 'ipv6'. This is ignored")

        self._dd_client = DogStatsd(host=host,
                                    port=port,
                                    namespace=prefix,
                                    max_buffer_size=maxudpsize)

    def timer(self, stat, rate=1, tags=None):
        self._dd_client.timed(metric=stat,
                              sample_rate=rate,
                              use_ms=True,
                              tags=tags)

    def timing(self, stat, delta, rate=1, tags=None):
        """Send new timing information. `delta` is in milliseconds."""
        self._dd_client.timing(metric=stat,
                               value=delta,
                               sample_rate=rate,
                               tags=tags)

    def incr(self, stat, count=1, rate=1, tags=None):
        """Increment a stat by `count`."""
        self._dd_client.increment(metric=stat,
                                  value=count,
                                  sample_rate=rate,
                                  tags=tags)

    def decr(self, stat, count=1, rate=1, tags=None):
        """Decrement a stat by `count`."""
        self._dd_client.decrement(metric=stat,
                                  value=count,
                                  sample_rate=rate,
                                  tags=tags)

    def gauge(self, stat, value, rate=1, delta=False, tags=None):
        """Set a gauge value."""
        self._dd_client.gauge(metric=stat,
                              value=value,
                              sample_rate=rate,
                              tags=tags)
        if delta:
            _log.warning(
                "DogStatsdAdapter was passed a gauge with 'delta' set. This is ignored in datadog"
            )

    def set(self, stat, value, rate=1, tags=None):
        """Set a set value."""
        self._dd_client.set(metric=stat,
                            value=value,
                            sample_rate=rate,
                            tags=tags)

    def histogram(self, stat, value, rate=1, tags=None):
        """
        Sample a histogram value, optionally setting tags and a sample rate.
        This is not supported by statsd.StatsClient. Use with caution!
        """
        self._dd_client.set(metric=stat,
                            value=value,
                            sample_rate=rate,
                            tags=tags)
예제 #17
0
            pass
        def __exit__(self, type, value, traceback):
            pass
        def __call__(self, func):
            def wrapped(*args, **kwargs):
                return func(*args, **kwargs)
            return wrapped

    def timed(*args, **kwargs):
            return WithDecorator()

    stats.timed = timed
else:
    from datadog.dogstatsd import DogStatsd
    stats = DogStatsd(host=os.getenv('STATSD_HOST', 'localhost'),
                      port=int(os.getenv('STATSD_PORT', 9125)),
                      namespace=os.getenv('STATSD_PREFIX', 'openstack')
                      )


##
# oslo.vmware.vim_util

def _get_token(retrieve_result):
    """Get token from result to obtain next set of results.
    :retrieve_result: Result of RetrievePropertiesEx API call
    :returns: token to obtain next set of results; None if no more results.
    """
    return getattr(retrieve_result, 'token', None)


def cancel_retrieval(si, retrieve_result):
예제 #18
0
 def _get_client(self, host, port, namespace):
     return DogStatsd(host=host, port=port, namespace=namespace)
예제 #19
0
class OpenStackRateLimitMiddleware(object):
    """
    OpenStack Rate Limit Middleware enforces configurable rate limits.

    Per combination of:
      service         ( compute, identity, object-store, .. )
      scope           ( initiator|target project uid, initiator host address )
      target_type_uri ( service/compute/servers, service/storage/block/volumes,.. )
      action          ( create, read, update, delete, authenticate, .. )
    """
    def __init__(self, app, **conf):
        self.app = app
        # Configuration via paste.ini.
        self.__conf = conf
        self.logger = log.Logger(conf.get('log_name', __name__))

        # StatsD is used to emit metrics.
        statsd_host = self.__conf.get('statsd_host', '127.0.0.1')
        statsd_port = common.to_int(self.__conf.get('statsd_port', 9125))
        statsd_prefix = self.__conf.get('statsd_prefix',
                                        common.Constants.metric_prefix)

        # Init StatsD client.
        self.metricsClient = DogStatsd(
            host=os.getenv('STATSD_HOST', statsd_host),
            port=int(os.getenv('STATSD_PORT', statsd_port)),
            namespace=os.getenv('STATSD_PREFIX', statsd_prefix))

        # Get backend configuration.
        # Backend is used to store count of requests.
        self.backend_host = self.__conf.get('backend_host', '127.0.0.1')
        self.backend_port = common.to_int(self.__conf.get('backend_port'),
                                          6379)
        self.logger.debug("using backend '{0}' on '{1}:{2}'".format(
            'redis', self.backend_host, self.backend_port))
        backend_timeout_seconds = common.to_int(
            self.__conf.get('backend_timeout_seconds'), 20)
        backend_max_connections = common.to_int(
            self.__conf.get('backend_max_connections'), 100)

        # Load configuration file.
        self.config = {}
        config_file = self.__conf.get('config_file', None)
        if config_file:
            try:
                self.config = common.load_config(config_file)
            except errors.ConfigError as e:
                self.logger.warning("error loading configuration: {0}".format(
                    str(e)))

        self.service_type = self.__conf.get('service_type', None)

        # This is required to trim the prefix from the target_type_uri.
        # Example:
        #   service_type      = identity
        #   cadf_service_name = data/security
        #   target_type_uri   = data/security/auth/tokens -> auth/tokens
        self.cadf_service_name = self.__conf.get('cadf_service_name', None)
        if common.is_none_or_unknown(self.cadf_service_name):
            self.cadf_service_name = common.CADF_SERVICE_TYPE_PREFIX_MAP.get(
                self.service_type, None)

        # Use configured parameters or ensure defaults.
        max_sleep_time_seconds = common.to_int(
            self.__conf.get(common.Constants.max_sleep_time_seconds), 20)
        log_sleep_time_seconds = common.to_int(
            self.__conf.get(common.Constants.log_sleep_time_seconds), 10)

        # Setup ratelimit and blacklist response.
        self._setup_response()

        # White-/blacklist can contain project, domain, user ids or the client ip address.
        # Don't apply rate limits to localhost.
        default_whitelist = ['127.0.0.1', 'localhost']
        config_whitelist = self.config.get('whitelist', [])
        self.whitelist = default_whitelist + config_whitelist
        self.whitelist_users = self.config.get('whitelist_users', [])

        self.blacklist = self.config.get('blacklist', [])
        self.blacklist_users = self.config.get('blacklist_users', [])

        # Mapping of potentially multiple CADF actions to one action.
        self.rate_limit_groups = self.config.get('groups', {})

        # Configurable scope in which a rate limit is applied. Defaults to initiator project id.
        # Rate limits are applied based on the tuple of (rate_limit_by, action, target_type_uri).
        self.rate_limit_by = self.__conf.get(
            'rate_limit_by', common.Constants.initiator_project_id)

        # Accuracy of the request timestamps used. Defaults to nanosecond accuracy.
        clock_accuracy = int(
            1 / units.Units.parse(self.__conf.get('clock_accuracy', '1ns')))

        self.backend = rate_limit_backend.RedisBackend(
            host=self.backend_host,
            port=self.backend_port,
            rate_limit_response=self.ratelimit_response,
            max_sleep_time_seconds=max_sleep_time_seconds,
            log_sleep_time_seconds=log_sleep_time_seconds,
            timeout_seconds=backend_timeout_seconds,
            max_connections=backend_max_connections,
            clock_accuracy=clock_accuracy,
        )

        # Test if the backend is ready.
        is_available, msg = self.backend.is_available()
        if not is_available:
            self.logger.warning(
                "rate limit not possible. the backend is not available: {0}".
                format(msg))

        # Provider for rate limits. Defaults to configuration file.
        # Also supports Limes.
        configuration_ratelimit_provider = provider.ConfigurationRateLimitProvider(
            service_type=self.service_type)

        # Force load of rate limits from configuration file.
        configuration_ratelimit_provider.read_rate_limits_from_config(
            config_file)
        self.ratelimit_provider = configuration_ratelimit_provider

        # If limes is enabled and we want to rate limit by initiator|target project id,
        # Set LimesRateLimitProvider as the provider for rate limits.
        limes_enabled = self.__conf.get('limes_enabled', False)
        if limes_enabled:
            self.__setup_limes_ratelimit_provider()

        self.logger.info("OpenStack Rate Limit Middleware ready for requests.")

    def _setup_response(self):
        """Setup configurable RateLimitExceededResponse and BlacklistResponse."""
        # Default responses.
        ratelimit_response = response.RateLimitExceededResponse()
        blacklist_response = response.BlacklistResponse()

        # Overwrite default responses if custom ones are configured.
        try:
            ratelimit_response_config = self.config.get(
                common.Constants.ratelimit_response)
            if ratelimit_response_config:
                status, status_code, headers, body, json_body = \
                    response.response_parameters_from_config(ratelimit_response_config)

                # Only create custom response if all parameters are given.
                if status and status_code and (body or json_body):
                    ratelimit_response = response.RateLimitExceededResponse(
                        status=status,
                        status_code=status_code,
                        headerlist=headers,
                        body=body,
                        json_body=json_body)

            blacklist_response_config = self.config.get(
                common.Constants.blacklist_response)
            if blacklist_response_config:
                status, status_code, headers, body, json_body = \
                    response.response_parameters_from_config(blacklist_response_config)

                # Only create custom response if all parameters are given.
                if status and status_code and (body or json_body):
                    blacklist_response = response.BlacklistResponse(
                        status=status,
                        status_code=status_code,
                        headerlist=headers,
                        body=body,
                        json_body=json_body)

        except Exception as e:
            self.logger.debug(
                "error configuring custom responses. falling back to defaults: {0}"
                .format(str(e)))

        finally:
            self.ratelimit_response = ratelimit_response
            self.blacklist_response = blacklist_response

    def __setup_limes_ratelimit_provider(self):
        """Setup Limes as provider for rate limits. If not successful fallback to configuration file."""
        try:
            limes_ratelimit_provider = provider.LimesRateLimitProvider(
                service_type=self.service_type,
                redis_host=self.backend_host,
                redis_port=self.backend_port,
                refresh_interval_seconds=self.__conf.get(
                    common.Constants.limes_refresh_interval_seconds, 300),
                limes_api_uri=self.__conf.get(common.Constants.limes_api_uri),
                auth_url=self.__conf.get('identity_auth_url'),
                username=self.__conf.get('username'),
                user_domain_name=self.__conf.get('user_domain_name'),
                password=self.__conf.get('password'),
                domain_name=self.__conf.get('domain_name'))
            self.ratelimit_provider = limes_ratelimit_provider

        except Exception as e:
            self.logger.debug(
                "failed to setup limes rate limit provider: {0}".format(
                    str(e)))

    @classmethod
    def factory(cls, global_config, **local_config):
        conf = global_config.copy()
        conf.update(local_config)

        def limiter(app):
            return cls(app, **conf)

        return limiter

    def _rate_limit(self, scope, action, target_type_uri, **kwargs):
        """
        Check the whitelist, blacklist, global and local ratelimits.

        :param scope: the scope of the request
        :param action: the action of the request
        :param target_type_uri: the target type URI of the response
        :return: None or BlacklistResponse or RateLimitResponse
        """
        # Labels used for all metrics.
        metric_labels = [
            'service:{0}'.format(self.service_type),
            'service_name:{0}'.format(self.cadf_service_name),
            'action:{0}'.format(action),
            '{0}:{1}'.format(self.rate_limit_by, scope),
            'target_type_uri:{0}'.format(target_type_uri)
        ]
        global_metric_labels = metric_labels + ['level:global']
        local_metric_labels = metric_labels + ['level:local']

        # Check whether a set of CADF actions are accounted together.
        new_action = self.get_action_from_rate_limit_groups(action)
        if not common.is_none_or_unknown(new_action):
            action = new_action
            metric_labels.append('action_group:{0}'.format(action))

        # Get CADF service name and trim from target_type_uri.
        trimmed_target_type_uri = target_type_uri
        if not common.is_none_or_unknown(self.cadf_service_name):
            trimmed_target_type_uri = self._trim_cadf_service_prefix_from_target_type_uri(
                self.cadf_service_name, target_type_uri)

        username = kwargs.get('username', None)
        # If we have the username: Check whether the user is white- or blacklisted.
        if username:
            metric_labels.append('initiator_user_name:{}'.format(username))
            if self.is_user_whitelisted(username):
                self.logger.debug(
                    "user {0} is whitelisted. skipping rate limit".format(
                        username))
                self.metricsClient.increment(
                    common.Constants.metric_requests_whitelisted_total,
                    tags=metric_labels)
                return None

            if self.is_user_blacklisted(username):
                self.logger.debug(
                    "user {0} is blacklisted. returning BlacklistResponse".
                    format(username))
                self.metricsClient.increment(
                    common.Constants.metric_requests_blacklisted_total,
                    tags=metric_labels)
                return self.blacklist_response

        # The key of the scope in the format $domainName/projectName.
        scope_name_key = kwargs.get('scope_name_key', None)
        if scope_name_key:
            metric_labels.append(
                'initiator_project_name:{}'.format(scope_name_key))

        # Check whitelist. If scope is whitelisted break here and don't apply any rate limits.
        if self.is_scope_whitelisted(scope) or self.is_scope_whitelisted(
                scope_name_key):
            self.logger.debug(
                "scope {0} (key: {1}) is whitelisted. skipping rate limit".
                format(scope, scope_name_key))
            self.metricsClient.increment(
                common.Constants.metric_requests_whitelisted_total,
                tags=metric_labels)
            return None

        # Check blacklist. If scope is blacklisted return BlacklistResponse.
        if self.is_scope_blacklisted(scope) or self.is_scope_blacklisted(
                scope_name_key):
            self.logger.debug(
                "scope {0} (key: {1}) is blacklisted. returning BlacklistResponse"
                .format(scope, scope_name_key))
            self.metricsClient.increment(
                common.Constants.metric_requests_blacklisted_total,
                tags=metric_labels)
            return self.blacklist_response

        # Get global rate limits from the provider.
        global_rate_limit = self.ratelimit_provider.get_global_rate_limits(
            action, trimmed_target_type_uri)

        # Don't rate limit if limit=-1 or unknown.
        if not common.is_unlimited(global_rate_limit):
            self.logger.debug(
                "global rate limit configured for request with action '{0}', target type URI '{1}': '{2}'"
                .format(action, target_type_uri, global_rate_limit))

            # Check global rate limits.
            # Global rate limits enforce a backend protection by counting all requests independent of their scope.
            rate_limit_response = self.backend.rate_limit(
                scope=None,
                action=action,
                target_type_uri=trimmed_target_type_uri,
                max_rate_string=global_rate_limit)
            if rate_limit_response:
                self.metricsClient.increment(
                    common.Constants.metric_requests_ratelimit_total,
                    tags=global_metric_labels)
                return rate_limit_response

        # Get local (for a certain scope) rate limits from provider.
        local_rate_limit = self.ratelimit_provider.get_local_rate_limits(
            scope, action, trimmed_target_type_uri)

        # Don't rate limit for rate_limit=-1 or if unknown.
        if not common.is_unlimited(local_rate_limit):
            self.logger.debug(
                "local rate limit configured for request with action '{0}', target type URI '{1}', scope '{2}': '{3}'"
                .format(action, target_type_uri, scope, local_rate_limit))

            # Check local (for a specific scope) rate limits.
            rate_limit_response = self.backend.rate_limit(
                scope=scope,
                action=action,
                target_type_uri=trimmed_target_type_uri,
                max_rate_string=local_rate_limit)
            if rate_limit_response:
                self.metricsClient.increment(
                    common.Constants.metric_requests_ratelimit_total,
                    tags=local_metric_labels)
                return rate_limit_response

        return None

    def __call__(self, environ, start_response):
        """
        WSGI entry point. Wraps environ in webob.Request.

        :param environ: the WSGI environment dict
        :param start_response: WSGI callable
        """
        # Save the app's response so it can be returned easily.
        resp = self.app

        try:
            self.metricsClient.open_buffer()

            # If the service type and/or service name is not configured,
            # attempt to extract watcher classification from environ and set it.
            self._set_service_type_and_name(environ)

            # Get openstack-watcher-middleware classification from requests environ.
            scope, action, target_type_uri = self.get_scope_action_target_type_uri_from_environ(
                environ)

            # Don't rate limit if any of scope, action, target type URI cannot be determined.
            if common.is_none_or_unknown(scope) or \
               common.is_none_or_unknown(action) or \
               common.is_none_or_unknown(target_type_uri):
                path = str(environ.get('PATH_INFO', common.Constants.unknown))
                method = str(
                    environ.get('REQUEST_METHOD', common.Constants.unknown))
                self.logger.debug(
                    "unknown request: action: {0}, target_type_uri: {1}, scope: {2}, method: {3}, path: {4}"
                    .format(action, target_type_uri, scope, method, path))

                self.metricsClient.increment(
                    common.Constants.metric_requests_unknown_classification,
                    tags=[
                        'service:{0}'.format(self.service_type),
                        'service_name:{0}'.format(self.cadf_service_name),
                    ])
                return

            # Returns a RateLimitResponse or BlacklistResponse or None, in which case the original response is returned.
            rate_limit_response = self._rate_limit(
                scope=scope,
                action=action,
                target_type_uri=target_type_uri,
                scope_name_key=self._get_scope_name_key_from_environ(environ),
                username=self._get_username_from_environ(environ),
            )
            if rate_limit_response:
                rate_limit_response.set_environ(environ)
                resp = rate_limit_response

        except Exception as e:
            self.metricsClient.increment(common.Constants.metric_errors_total)
            self.logger.debug("checking rate limits failed with: {0}".format(
                str(e)))

        finally:
            self.metricsClient.close_buffer()
            return resp(environ, start_response)

    def is_scope_blacklisted(self, key_to_check):
        """
        Check whether a scope (user_id, project_id or client ip) is blacklisted.

        :param key_to_check: the user, project uid or client ip
        :return: bool whether the key is blacklisted
        """
        for entry in self.blacklist:
            if entry == key_to_check:
                return True
        return False

    def is_user_blacklisted(self, user_to_check):
        """
        Check whether a user is blacklisted.

        :param user_to_check: the name of the user to check
        :return: bool whether user is blacklisted
        """
        for u in self.blacklist_users:
            if str(u).lower() == str(user_to_check).lower():
                return True
        return False

    def is_scope_whitelisted(self, key_to_check):
        """
        Check whether a scope (user_id, project_id or client ip) is whitelisted.

        :param key_to_check: the user, project uid or client ip
        :return: bool whether the key is whitelisted
        """
        for entry in self.whitelist:
            if entry == key_to_check:
                return True
        return False

    def is_user_whitelisted(self, user_to_check):
        """
        Check whether a user is whitelisted.

        :param user_to_check: the name of the user to check
        :return: bool whether user is whitelisted
        """
        for u in self.whitelist_users:
            if str(u).lower() == str(user_to_check).lower():
                return True
        return False

    def get_scope_action_target_type_uri_from_environ(self, environ):
        """
        Get the scope, action, target type URI from the request environ.

        :param environ: the request environ
        :return: tuple of scope, action, target type URI
        """
        action = target_type_uri = scope = None
        try:
            # Get the CADF action.
            env_action = environ.get('WATCHER.ACTION')
            if not common.is_none_or_unknown(env_action):
                action = env_action

            # Get the target type URI.
            env_target_type_uri = environ.get('WATCHER.TARGET_TYPE_URI')
            if not common.is_none_or_unknown(env_target_type_uri):
                target_type_uri = env_target_type_uri

            # Get scope from request environment, which might be an initiator.project_id, target.project_id, etc. .
            env_scope = self._get_scope_from_environ(environ)
            if not common.is_none_or_unknown(env_scope):
                scope = env_scope

        except Exception as e:
            self.logger.debug(
                "error while getting scope, action, target type URI from environ: {0}"
                .format(str(e)))

        finally:
            self.logger.debug(
                'got WATCHER.* attributes from environ: action: {0}, target_type_uri: {1}, scope: {2}'
                .format(action, target_type_uri, scope))
            return scope, action, target_type_uri

    def _get_scope_from_environ(self, environ):
        """
        Get the scope from the requests environ.
        The scope is configurable and may be the target|initiator project uid or the initiator host address.
        Default to initiator project ID.

        :param environ: the requests environ
        :return: the scope
        """
        scope = None
        if self.rate_limit_by == common.Constants.target_project_id:
            env_scope = environ.get('WATCHER.TARGET_PROJECT_ID', None)
        elif self.rate_limit_by == common.Constants.initiator_host_address:
            env_scope = environ.get('WATCHER.INITIATOR_HOST_ADDRESS', None)
        else:
            env_scope = environ.get('WATCHER.INITIATOR_PROJECT_ID', None)

        # Ensure the scope is not 'unknown'.
        if not common.is_none_or_unknown(env_scope):
            scope = env_scope
        return scope

    def _get_scope_name_key_from_environ(self, environ):
        """
        Attempt to build the key '$domainName/$projectName' from the WATCHER attributes found in the request environ.

        :param environ: the request environ
        :return: the key or None
        """
        _domain_name = environ.get('WATCHER.INITIATOR_DOMAIN_NAME', None)
        _project_domain_name = environ.get(
            'WATCHER.INITIATOR_PROJECT_DOMAIN_NAME', None)
        project_name = environ.get('WATCHER.INITIATOR_PROJECT_NAME', None)
        domain_name = _project_domain_name or _domain_name

        if common.is_none_or_unknown(
                project_name) or common.is_none_or_unknown(domain_name):
            return None
        return '{0}/{1}'.format(domain_name, project_name)

    def _get_username_from_environ(self, environ):
        """
        Attempt to get username from WATCHER attributes found in request environ.

        :param environ: the request environ
        :return: the username or None
        """
        username = environ.get('WATCHER.INITIATOR_USER_NAME', None)
        if common.is_none_or_unknown(username):
            return None
        return username

    def _set_service_type_and_name(self, environ):
        """
        Set the service type and name according to the watchers classification passed in the request WSGI environ.
        Used if nothing was configured.

        :param environ: the request WSGI environment
        """
        # Get service type from request environ.
        if common.is_none_or_unknown(self.service_type):
            svc_type = environ.get('WATCHER.SERVICE_TYPE')
            if not common.is_none_or_unknown(svc_type):
                self.service_type = svc_type
                self.ratelimit_provider.service_type = self.service_type

        # set service name from environ
        if common.is_none_or_unknown(self.cadf_service_name):
            svc_name = environ.get('WATCHER.CADF_SERVICE_NAME')
            if not common.is_none_or_unknown(svc_name):
                self.cadf_service_name = svc_name
                self.ratelimit_provider.cadf_service_name = self.cadf_service_name

    def _trim_cadf_service_prefix_from_target_type_uri(self, prefix,
                                                       target_type_uri):
        """
        Get cadf service name and trim from target_type_uri.

        Example:
            target_type_uri:            service/storage/object/account/container/object
            cadf_service_name:          service/storage/object
            => trimmed_target_type_uri: account/container/object

        :param prefix: the cadf service name prefixing the target_type_uri
        :param target_type_uri: the target_type_uri with the prefix
        :return: target_type_uri without prefix
        """
        target_type_uri_without_prefix = target_type_uri
        try:
            without_prefix = target_type_uri.split(prefix)
            if len(without_prefix) != 2:
                raise IndexError
            target_type_uri_without_prefix = without_prefix[-1].lstrip('/')
        except IndexError as e:
            self.logger.warning(
                "rate limiting might not be possible. cannot trim prefix '{0}' from target_type_uri '{1}': {2}"
                .format(prefix, target_type_uri, str(e)))
        finally:
            return target_type_uri_without_prefix

    def get_action_from_rate_limit_groups(self, action):
        """
        Multiple CADF actions can be grouped and accounted as one entity.

        :param action: the original CADF action
        :return: the original action or action as per grouping
        """
        for group in self.rate_limit_groups:
            if action in self.rate_limit_groups[group]:
                return group
        return action
예제 #20
0
    def __init__(self, app, **conf):
        self.app = app
        # Configuration via paste.ini.
        self.__conf = conf
        self.logger = log.Logger(conf.get('log_name', __name__))

        # StatsD is used to emit metrics.
        statsd_host = self.__conf.get('statsd_host', '127.0.0.1')
        statsd_port = common.to_int(self.__conf.get('statsd_port', 9125))
        statsd_prefix = self.__conf.get('statsd_prefix',
                                        common.Constants.metric_prefix)

        # Init StatsD client.
        self.metricsClient = DogStatsd(
            host=os.getenv('STATSD_HOST', statsd_host),
            port=int(os.getenv('STATSD_PORT', statsd_port)),
            namespace=os.getenv('STATSD_PREFIX', statsd_prefix))

        # Get backend configuration.
        # Backend is used to store count of requests.
        self.backend_host = self.__conf.get('backend_host', '127.0.0.1')
        self.backend_port = common.to_int(self.__conf.get('backend_port'),
                                          6379)
        self.logger.debug("using backend '{0}' on '{1}:{2}'".format(
            'redis', self.backend_host, self.backend_port))
        backend_timeout_seconds = common.to_int(
            self.__conf.get('backend_timeout_seconds'), 20)
        backend_max_connections = common.to_int(
            self.__conf.get('backend_max_connections'), 100)

        # Load configuration file.
        self.config = {}
        config_file = self.__conf.get('config_file', None)
        if config_file:
            try:
                self.config = common.load_config(config_file)
            except errors.ConfigError as e:
                self.logger.warning("error loading configuration: {0}".format(
                    str(e)))

        self.service_type = self.__conf.get('service_type', None)

        # This is required to trim the prefix from the target_type_uri.
        # Example:
        #   service_type      = identity
        #   cadf_service_name = data/security
        #   target_type_uri   = data/security/auth/tokens -> auth/tokens
        self.cadf_service_name = self.__conf.get('cadf_service_name', None)
        if common.is_none_or_unknown(self.cadf_service_name):
            self.cadf_service_name = common.CADF_SERVICE_TYPE_PREFIX_MAP.get(
                self.service_type, None)

        # Use configured parameters or ensure defaults.
        max_sleep_time_seconds = common.to_int(
            self.__conf.get(common.Constants.max_sleep_time_seconds), 20)
        log_sleep_time_seconds = common.to_int(
            self.__conf.get(common.Constants.log_sleep_time_seconds), 10)

        # Setup ratelimit and blacklist response.
        self._setup_response()

        # White-/blacklist can contain project, domain, user ids or the client ip address.
        # Don't apply rate limits to localhost.
        default_whitelist = ['127.0.0.1', 'localhost']
        config_whitelist = self.config.get('whitelist', [])
        self.whitelist = default_whitelist + config_whitelist
        self.whitelist_users = self.config.get('whitelist_users', [])

        self.blacklist = self.config.get('blacklist', [])
        self.blacklist_users = self.config.get('blacklist_users', [])

        # Mapping of potentially multiple CADF actions to one action.
        self.rate_limit_groups = self.config.get('groups', {})

        # Configurable scope in which a rate limit is applied. Defaults to initiator project id.
        # Rate limits are applied based on the tuple of (rate_limit_by, action, target_type_uri).
        self.rate_limit_by = self.__conf.get(
            'rate_limit_by', common.Constants.initiator_project_id)

        # Accuracy of the request timestamps used. Defaults to nanosecond accuracy.
        clock_accuracy = int(
            1 / units.Units.parse(self.__conf.get('clock_accuracy', '1ns')))

        self.backend = rate_limit_backend.RedisBackend(
            host=self.backend_host,
            port=self.backend_port,
            rate_limit_response=self.ratelimit_response,
            max_sleep_time_seconds=max_sleep_time_seconds,
            log_sleep_time_seconds=log_sleep_time_seconds,
            timeout_seconds=backend_timeout_seconds,
            max_connections=backend_max_connections,
            clock_accuracy=clock_accuracy,
        )

        # Test if the backend is ready.
        is_available, msg = self.backend.is_available()
        if not is_available:
            self.logger.warning(
                "rate limit not possible. the backend is not available: {0}".
                format(msg))

        # Provider for rate limits. Defaults to configuration file.
        # Also supports Limes.
        configuration_ratelimit_provider = provider.ConfigurationRateLimitProvider(
            service_type=self.service_type)

        # Force load of rate limits from configuration file.
        configuration_ratelimit_provider.read_rate_limits_from_config(
            config_file)
        self.ratelimit_provider = configuration_ratelimit_provider

        # If limes is enabled and we want to rate limit by initiator|target project id,
        # Set LimesRateLimitProvider as the provider for rate limits.
        limes_enabled = self.__conf.get('limes_enabled', False)
        if limes_enabled:
            self.__setup_limes_ratelimit_provider()

        self.logger.info("OpenStack Rate Limit Middleware ready for requests.")
예제 #21
0
class OpenStackWatcherMiddleware(object):
    """
    OpenStack Watcher Middleware

    Watches OpenStack traffic and classifies according to CADF standard
    """
    def __init__(self, app, config, logger=logging.getLogger(__name__)):
        self.logger = logger
        self.app = app
        self.wsgi_config = config
        self.watcher_config = {}

        self.cadf_service_name = self.wsgi_config.get('cadf_service_name',
                                                      None)
        self.service_type = self.wsgi_config.get('service_type',
                                                 taxonomy.UNKNOWN)
        # get the project uid from the request path or from the token (default)
        self.is_project_id_from_path = common.string_to_bool(
            self.wsgi_config.get('target_project_id_from_path', 'False'))
        # get the project id from the service catalog (see documentation on keystone auth_token middleware)
        self.is_project_id_from_service_catalog = common.string_to_bool(
            self.wsgi_config.get('target_project_id_from_service_catalog',
                                 'False'))

        # whether to include the target project id in the metrics
        self.is_include_target_project_id_in_metric = common.string_to_bool(
            self.wsgi_config.get('include_target_project_id_in_metric',
                                 'True'))
        # whether to include the target domain id in the metrics
        self.is_include_target_domain_id_in_metric = common.string_to_bool(
            self.wsgi_config.get('include_target_domain_id_in_metric', 'True'))
        # whether to include the initiator user id in the metrics
        self.is_include_initiator_user_id_in_metric = common.string_to_bool(
            self.wsgi_config.get('include_initiator_user_id_in_metric',
                                 'False'))

        config_file_path = config.get('config_file', None)
        if config_file_path:
            try:
                self.watcher_config = load_config(config_file_path)
            except errors.ConfigError as e:
                self.logger.debug("custom actions not available: %s", str(e))

        custom_action_config = self.watcher_config.get('custom_actions', {})
        path_keywords = self.watcher_config.get('path_keywords', {})
        keyword_exclusions = self.watcher_config.get('keyword_exclusions', {})
        regex_mapping = self.watcher_config.get('regex_path_mapping', {})

        # init the strategy used to determine the target type uri
        strat = STRATEGIES.get(self.service_type, strategies.BaseCADFStrategy)

        # set custom prefix to target type URI or use defaults
        target_type_uri_prefix = common.SERVICE_TYPE_CADF_PREFIX_MAP.get(
            self.service_type, 'service/{0}'.format(self.service_type))

        if self.cadf_service_name:
            target_type_uri_prefix = self.cadf_service_name

        strategy = strat(target_type_uri_prefix=target_type_uri_prefix,
                         path_keywords=path_keywords,
                         keyword_exclusions=keyword_exclusions,
                         custom_action_config=custom_action_config,
                         regex_mapping=regex_mapping)

        self.strategy = strategy

        self.metric_client = DogStatsd(
            host=self.wsgi_config.get("statsd_host", "127.0.0.1"),
            port=int(self.wsgi_config.get("statsd_port", 9125)),
            namespace=self.wsgi_config.get("statsd_namespace",
                                           "openstack_watcher"))

    @classmethod
    def factory(cls, global_config, **local_config):
        conf = global_config.copy()
        conf.update(local_config)

        def watcher(app):
            return cls(app, conf)

        return watcher

    def __call__(self, environ, start_response):
        """
        WSGI entry point. Wraps environ in webob.Request

        :param environ: the WSGI environment dict
        :param start_response: WSGI callable
        """

        # capture start timestamp
        start = time.time()

        req = Request(environ)

        # determine initiator based on token context
        initiator_project_id = self.get_safe_from_environ(
            environ, 'HTTP_X_PROJECT_ID')
        initiator_project_name = self.get_safe_from_environ(
            environ, 'HTTP_X_PROJECT_NAME')
        initiator_project_domain_id = self.get_safe_from_environ(
            environ, 'HTTP_X_PROJECT_DOMAIN_ID')
        initiator_project_domain_name = self.get_safe_from_environ(
            environ, 'HTTP_X_PROJECT_DOMAIN_NAME')
        initiator_domain_id = self.get_safe_from_environ(
            environ, 'HTTP_X_DOMAIN_ID')
        initiator_domain_name = self.get_safe_from_environ(
            environ, 'HTTP_X_DOMAIN_NAME')
        initiator_user_id = self.get_safe_from_environ(environ,
                                                       'HTTP_X_USER_ID')
        initiator_user_name = self.get_safe_from_environ(
            environ, 'HTTP_X_USER_NAME')
        initiator_user_domain_id = self.get_safe_from_environ(
            environ, 'HTTP_X_USER_DOMAIN_ID')
        initiator_user_domain_name = self.get_safe_from_environ(
            environ, 'HTTP_X_USER_DOMAIN_NAME')
        initiator_host_address = req.client_addr or taxonomy.UNKNOWN

        # determine target based on request path or keystone.token_info
        target_project_id = taxonomy.UNKNOWN
        if self.is_project_id_from_path:
            target_project_id = self.get_target_project_uid_from_path(req.path)
        elif self.is_project_id_from_service_catalog:
            target_project_id = self.get_target_project_id_from_keystone_token_info(
                environ.get('keystone.token_info'))

        # default target_project_id to initiator_project_id if still unknown
        if not target_project_id or target_project_id == taxonomy.UNKNOWN:
            target_project_id = initiator_project_id

        # determine target.type_uri for request
        target_type_uri = self.determine_target_type_uri(req)

        # determine cadf_action for request. consider custom action config.
        cadf_action = self.determine_cadf_action(req, target_type_uri)

        # if authentication request consider project, domain and user in body
        if self.service_type == 'identity' and cadf_action == taxonomy.ACTION_AUTHENTICATE:
            initiator_project_id, initiator_domain_id, initiator_user_id = \
                self.get_project_domain_and_user_id_from_keystone_authentication_request(req)

        # set environ for initiator
        environ['WATCHER.INITIATOR_PROJECT_ID'] = initiator_project_id
        environ['WATCHER.INITIATOR_PROJECT_NAME'] = initiator_project_name
        environ[
            'WATCHER.INITIATOR_PROJECT_DOMAIN_ID'] = initiator_project_domain_id
        environ[
            'WATCHER.INITIATOR_PROJECT_DOMAIN_NAME'] = initiator_project_domain_name
        environ['WATCHER.INITIATOR_DOMAIN_ID'] = initiator_domain_id
        environ['WATCHER.INITIATOR_DOMAIN_NAME'] = initiator_domain_name
        environ['WATCHER.INITIATOR_USER_ID'] = initiator_user_id
        environ['WATCHER.INITIATOR_USER_NAME'] = initiator_user_name
        environ['WATCHER.INITIATOR_USER_DOMAIN_ID'] = initiator_user_domain_id
        environ[
            'WATCHER.INITIATOR_USER_DOMAIN_NAME'] = initiator_user_domain_name
        environ['WATCHER.INITIATOR_HOST_ADDRESS'] = initiator_host_address

        # set environ for target
        environ['WATCHER.TARGET_PROJECT_ID'] = target_project_id
        environ['WATCHER.TARGET_TYPE_URI'] = target_type_uri

        # general cadf attributes
        environ['WATCHER.ACTION'] = cadf_action
        environ['WATCHER.SERVICE_TYPE'] = self.service_type
        environ[
            'WATCHER.CADF_SERVICE_NAME'] = self.strategy.get_cadf_service_name(
            )

        # labels applied to all metrics emitted by this middleware
        labels = [
            "service_name:{0}".format(self.strategy.get_cadf_service_name()),
            "service:{0}".format(self.service_type),
            "action:{0}".format(cadf_action),
            "target_type_uri:{0}".format(target_type_uri),
        ]

        # additional labels not needed in all metrics
        detail_labels = [
            "initiator_project_id:{0}".format(initiator_project_id),
            "initiator_domain_id:{0}".format(initiator_domain_id),
        ]
        detail_labels = labels + detail_labels

        # include the target project id in metric
        if self.is_include_target_project_id_in_metric:
            detail_labels.append(
                "target_project_id:{0}".format(target_project_id))

        # include initiator user id
        if self.is_include_initiator_user_id_in_metric:
            detail_labels.append(
                "initiator_user_id:{0}".format(initiator_user_id))

        # if swift request: determine target.container_id based on request path
        if common.is_swift_request(
                req.path) or self.service_type == 'object-store':
            _, target_container_id = self.get_target_account_container_id_from_request(
                req)
            environ['WATCHER.TARGET_CONTAINER_ID'] = target_container_id

        self.logger.debug(
            'got request with initiator_project_id: {0}, initiator_domain_id: {1}, initiator_user_id: {2}, '
            'target_project_id: {3}, action: {4}, target_type_uri: {5}'.format(
                initiator_project_id, initiator_domain_id, initiator_user_id,
                target_project_id, cadf_action, target_type_uri))

        # capture the response status
        response_wrapper = {}

        try:

            def _start_response_wrapper(status, headers, exc_info=None):
                response_wrapper.update(status=status,
                                        headers=headers,
                                        exc_info=exc_info)
                return start_response(status, headers, exc_info)

            return self.app(environ, _start_response_wrapper)
        finally:
            try:
                self.metric_client.open_buffer()

                status = response_wrapper.get('status')
                if status:
                    status_code = status.split()[0]
                else:
                    status_code = taxonomy.UNKNOWN

                labels.append("status:{0}".format(status_code))
                detail_labels.append("status:{0}".format(status_code))

                self.metric_client.timing(
                    'api_requests_duration_seconds',
                    int(round(1000 * (time.time() - start))),
                    tags=labels)
                self.metric_client.increment('api_requests_total',
                                             tags=detail_labels)
            except Exception as e:
                self.logger.debug("failed to submit metrics for %s: %s" %
                                  (str(labels), str(e)))
            finally:
                self.metric_client.close_buffer()

    def get_safe_from_environ(self, environ, key, default=taxonomy.UNKNOWN):
        """
        get value for a key from the environ dict ensuring it's never None or an empty string

        :param environ: the request environ
        :param key: the key in the environ dictionary
        :param default: return value if key not found
        :return: the value to the key or default
        """
        val = default
        try:
            v = environ.get(key, default)
            if v and v != "":
                val = v

        except Exception as e:
            self.logger.debug("error getting '{0}' from environ: {1}".format(
                key, e))

        finally:
            return val

    def get_target_project_uid_from_path(self, path):
        """
        get the project uid from the path, which should look like
        ../v1.2/<project_uid>/.. or ../v1/AUTH_<project_uid>/..

        :param path: the request path containing a project uid
        :return: the project uid
        """
        project_uid = taxonomy.UNKNOWN
        try:
            if common.is_swift_request(
                    path) and self.strategy.name == 'object-store':
                project_uid = self.strategy.get_swift_project_id_from_path(
                    path)
            else:
                project_uid = common.get_project_id_from_os_path()
        finally:
            if project_uid == taxonomy.UNKNOWN:
                self.logger.debug(
                    "unable to obtain target.project_id from request path '{0}'"
                    .format(path))
            else:
                self.logger.debug(
                    "request path '{0}' contains target.project_id '{1}'".
                    format(path, project_uid))
            return project_uid

    def get_target_project_id_from_keystone_token_info(self, token_info):
        """
        the token info dict contains the service catalog, in which the project specific
        endpoint urls per service can be found.

        :param token_info: token info dictionary
        :return: the project id or unknown
        """
        project_id = taxonomy.UNKNOWN
        try:
            service_catalog = token_info.get('token', {}).get('catalog', [])
            if not service_catalog:
                raise None

            for service in service_catalog:
                svc_type = service.get('type', None)
                if not svc_type or svc_type != self.service_type:
                    continue

                svc_endpoints = service.get('endpoints', None)
                if not svc_endpoints:
                    continue

                project_id = self._get_project_id_from_service_endpoints(
                    svc_endpoints)
                if project_id:
                    break

        except Exception as e:
            self.logger.debug(
                'unable to get target.project_id from service catalog: ',
                str(e))

        finally:
            if project_id == taxonomy.UNKNOWN:
                self.logger.debug(
                    "unable to get target.project_id '{0}' for service type '{1}' from service catalog"
                    .format(project_id, self.service_type))
            else:
                self.logger.debug(
                    "got target.project_id '{0}' for service type '{1}' from service catalog"
                    .format(project_id, self.service_type))
            return project_id

    def _get_project_id_from_service_endpoints(self,
                                               endpoint_list,
                                               endpoint_type=None):
        """
        get the project id from an endpoint url for a given type | type = {public,internal,admin}

        :param endpoint_list: list of endpoints
        :param endpoint_type: optional endpoint type
        :return: the project id or unknown
        """
        project_id = taxonomy.UNKNOWN
        try:
            for endpoint in endpoint_list:
                url = endpoint.get('url', None)
                type = endpoint.get('interface', None)
                if not url or not type:
                    continue

                if self.strategy.name == 'object-store':
                    project_id = self.strategy.get_swift_project_id_from_path(
                        url)
                else:
                    project_id = common.get_project_id_from_os_path(url)

                # break here if endpoint_type is given and types match
                if endpoint_type and endpoint_type.lower() == type.lower():
                    break
                # break here if no endpoint_type given but project id was found
                elif not endpoint_type and project_id != taxonomy.UNKNOWN:
                    break
        finally:
            if project_id == taxonomy.UNKNOWN:
                self.logger.debug(
                    "found no project id in endpoints for service type '{0}'".
                    format(self.service_type))
            else:
                self.logger.debug(
                    "found target project id '{0}' in endpoints for service type '{1}'"
                    .format(project_id, self.service_type))
            return project_id

    def get_project_domain_and_user_id_from_keystone_authentication_request(
            self, req):
        """
        get project, domain, user id from authentication request.
        used in combination with client_addr to determine which client authenticates in which scope

        :param req: the request
        :return: project_id, domain_id, user_id
        """
        project_id = domain_id = user_id = taxonomy.UNKNOWN
        try:
            if not req.json:
                return

            json_body_dict = common.load_json_dict(req.json)
            if not json_body_dict:
                return
            project_id = common.find_project_id_in_auth_dict(json_body_dict)
            domain_id = common.find_domain_id_in_auth_dict(json_body_dict)
            user_id = common.find_user_id_in_auth_dict(json_body_dict)
        except Exception as e:
            self.logger.debug(
                'unable to parse keystone authentication request body: {0}'.
                format(str(e)))
        finally:
            return project_id, domain_id, user_id

    def get_target_account_container_id_from_request(self, req):
        """
        get swift account id, container name from request

        :param req: the request
        :return: account uid, container name or unknown
        """
        # break here if we don't have the object-store strategy
        if self.strategy.name != 'object-store':
            return taxonomy.UNKNOWN, taxonomy.UNKNOWN

        account_id, container_id, _ = self.strategy.get_swift_account_container_object_id_from_path(
            req.path)
        return account_id, container_id

    def determine_target_type_uri(self, req):
        """
        determine the target type uri as per concrete strategy

        :param req: the request
        :return: the target type uri or taxonomy.UNKNOWN
        """
        target_type_uri = self.strategy.determine_target_type_uri(req)
        self.logger.debug(
            "target type URI of requests '{0} {1}' is '{2}'".format(
                req.method, req.path, target_type_uri))
        return target_type_uri

    def determine_cadf_action(self, req, target_type_uri=None):
        """
        attempts to determine the cadf action for a request in the following order:
        (1) return custom action if one is configured
        (2) if /action, /os-instance-action request, return action from request body
        (3) return action based on request method

        :param custom_action_config: configuration of custom actions
        :param target_type_uri: the target type URI
        :param req: the request
        :return: the cadf action or unknown
        """
        cadf_action = self.strategy.determine_cadf_action(req, target_type_uri)
        self.logger.debug("cadf action for '{0} {1}' is '{2}'".format(
            req.method, req.path, cadf_action))
        return cadf_action
예제 #22
0
class StatsdHandler(logging.Handler):

    DEFAULT_PUBLISH_TEMPLATES = {
        'default': ['%(logger)s;%(attr)s;%(metric_name)s']
    }

    DEFAULT_CONFIG = {
        'app_key': 'default_app_key',
        'host': 'localhost',
        'port': 8125,
        'sample_rate': 1,
        'disabled': False
    }

    def __init__(self, args=None, config_path=None):
        logging.Handler.__init__(self)
        if args is not None and args != '':
            config_path = args
        if not os.path.isfile(config_path):
            raise Exception('Invalid path to config file.')
        with open(config_path) as config_file_obj:
            self.config = load(config_file_obj.read())
        for key in self.DEFAULT_CONFIG:
            setattr(
                self, key,
                self.config.get('main', {}).get(key, None)
                or self.DEFAULT_CONFIG[key])

        # Initialize Statsd Client
        self.statsd = DogStatsd(host=self.host,
                                port=self.port,
                                namespace=self.app_key)

        self.publish_templates = self.DEFAULT_PUBLISH_TEMPLATES
        publish_templates = self.config.get('publish_templates', {})
        self.publish_templates.update(publish_templates)
        self.counters = self.config.get('counters', {})
        self.gauges = self.config.get('gauges', {})
        self.timers = self.config.get('timers', [])
        self.histograms = self.config.get('histograms', {})
        self.sets = self.config.get('sets', {})
        self.timers_start_keys = self._get_timers_keys_list('start')
        self.timers_end_keys = self._get_timers_keys_list('end')
        self.timers_value_keys = self._get_timers_keys_list('value')

    def _get_timers_keys_list(self, key_prefix):
        keys = []
        for t in self.timers:
            key = t.get('{}_attr_name'.format(key_prefix), None)
            if key is not None:
                keys.append(key)
        return keys

    def _get_timer_params(self,
                          key_prefix,
                          value_attr_name=None,
                          start_attr_name=None,
                          end_attr_name=None):
        for t in self.timers:
            if key_prefix == 'start':
                if (t.get('start_attr_name', None) == start_attr_name
                        and t.get('end_attr_name', None) == end_attr_name):
                    return [
                        t.get('name', start_attr_name),
                        t.get('publish_template', 'default')
                    ]
            elif key_prefix == 'value':
                if (t.get('value_attr_name', None) == value_attr_name):
                    return [
                        t.get('name', value_attr_name),
                        t.get('publish_template', 'default')
                    ]
            else:
                return None, None

    def _publish_count(self, subname, value):
        try:
            if float(value) > 0:
                self.statsd.increment(subname, value)
            else:
                self.statsd.decrement(subname, value)
        except:
            pass

    def _publish_timer(self, subname, value):
        try:
            self.statsd.timing(subname, value)
        except:
            pass

    def _process_counter_metrics(self, attr, record):
        lookup_value = self.counters[attr].get('lookup_value', 'None')
        value = getattr(record, attr, None)
        value_type = self.counters[attr].get('value_type', 'key')
        value_equals = self.counters[attr].get('value_equals', [])
        publish_template = self.counters[attr].get('publish_template',
                                                   'default')
        if publish_template not in self.publish_templates:
            publish_template = 'default'
        if value_type == 'value':
            counter_subname = attr
            counter_value = value if value is not None else 1
        elif value_type == 'key':
            counter_subname = value if value is not None else lookup_value
            if len(value_equals) > 0 and value not in value_equals:
                return
            counter_value = 1
        else:
            return
        for pt in self.publish_templates[publish_template]:
            subname = pt % dict(
                logger=record.name, attr=attr, metric_name=counter_subname)
            self._publish_count(subname, counter_value)

    def _process_timer_metrics(self, attr, record, key_prefix):
        if key_prefix == 'start':
            start_attr_value = getattr(record, attr, None)
            if start_attr_value is None:
                return
            start_attr_name = attr
            end_attr_value = None
            for end_attr_name in self.timers_end_keys:
                end_attr_value = getattr(record, end_attr_name, None)
                if end_attr_value is not None:
                    try:
                        timer_value = float(end_attr_value) -\
                            float(start_attr_value)
                        timer_name, publish_template =\
                            self._get_timer_params(
                                key_prefix, start_attr_name=start_attr_name,
                                end_attr_name=end_attr_name)
                        if publish_template not in self.publish_templates:
                            publish_template = 'default'
                        if timer_name is not None:
                            for pt in self.publish_templates[publish_template]:
                                subname = pt % dict(logger=record.name,
                                                    attr='',
                                                    metric_name=timer_name)
                                self._publish_timer(subname, timer_value)
                    except:
                        pass
        elif key_prefix == 'value':
            timer_value = getattr(record, attr, None)
            if timer_value is None:
                return
            timer_name, publish_template = self._get_timer_params(
                key_prefix, value_attr_name=attr)
            if publish_template not in self.publish_templates:
                publish_template = 'default'
            if timer_name is not None:
                for pt in self.publish_templates[publish_template]:
                    subname = pt % dict(
                        logger=record.name, attr='', metric_name=timer_name)
                    self._publish_timer(subname, timer_value)

    def _get_publish_template(self, metric_type, attr):
        metric_types_dict = getattr(self, metric_type, {})
        if metric_type == 'sets':
            publisher = self.statsd.set
        elif metric_type == 'gauges':
            publisher = self.statsd.gauge
        elif metric_type == 'histograms':
            publisher = self.statsd.histogram
        else:
            publisher = None
        publish_template =\
            metric_types_dict[attr].get('publish_template', 'default')
        if publish_template not in self.publish_templates:
            publish_template = 'default'
        return publish_template, publisher

    def _process_metrics(self, metric_type, attr, record):
        value = getattr(record, attr, None)
        if value is not None:
            publish_template, publisher =\
                self._get_publish_template(metric_type, attr)
            for pt in self.publish_templates[publish_template]:
                subname = pt % dict(
                    logger=record.name, attr=attr, metric_name=attr)
                publisher(subname, value)

    def emit(self, record):
        for attr in dir(record):
            if attr in self.counters:
                self._process_counter_metrics(attr, record)
            elif attr in self.gauges:
                self._process_metrics('gauges', attr, record)
            elif attr in self.timers_start_keys:
                self._process_timer_metrics(attr, record, 'start')
            elif attr in self.timers_value_keys:
                self._process_timer_metrics(attr, record, 'value')
            elif attr in self.histograms:
                self._process_metrics('histograms', attr, record)
            elif attr in self.sets:
                self._process_metrics('sets', attr, record)
            else:
                continue
예제 #23
0
    def setup(self):
        from datadog.dogstatsd import DogStatsd

        self.client = DogStatsd(host=self.host, port=self.port)