From 62d03b0d41828bc1899ea9ff8ba0e16461354a3a Mon Sep 17 00:00:00 2001 From: kevgliss Date: Fri, 1 Apr 2016 16:54:33 -0700 Subject: [PATCH] Closes #216 --- lemur/__init__.py | 12 ++- lemur/auth/views.py | 5 ++ lemur/certificates/service.py | 2 + lemur/extensions.py | 3 + lemur/metrics.py | 32 ++++++++ lemur/plugins/bases/metric.py | 16 ++++ lemur/plugins/lemur_atlas/__init__.py | 5 ++ lemur/plugins/lemur_atlas/plugin.py | 107 ++++++++++++++++++++++++++ setup.py | 3 +- 9 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 lemur/metrics.py create mode 100644 lemur/plugins/bases/metric.py create mode 100644 lemur/plugins/lemur_atlas/__init__.py create mode 100644 lemur/plugins/lemur_atlas/plugin.py diff --git a/lemur/__init__.py b/lemur/__init__.py index 5ceed3ea..6a8de79b 100644 --- a/lemur/__init__.py +++ b/lemur/__init__.py @@ -11,6 +11,7 @@ from __future__ import absolute_import, division, print_function from lemur import factory +from lemur.extensions import metrics from lemur.users.views import mod as users_bp from lemur.roles.views import mod as roles_bp @@ -70,8 +71,17 @@ def configure_hook(app): def after(response): return response + @app.errorhandler(500) + def internal_error(error): + metrics.send('500_status_code', 'counter', 1) + + @app.errorhandler(400) + def response_error(error): + metrics.send('400_status_code', 'counter', 1) + @app.errorhandler(PermissionDenied) - def handle_invalid_usage(error): + def permission_denied_error(error): + metrics.send('403_status_code', 'counter', 1) response = {'message': 'You are not allow to access this resource'} response.status_code = 403 return response diff --git a/lemur/auth/views.py b/lemur/auth/views.py index b5f828cb..c22762b2 100644 --- a/lemur/auth/views.py +++ b/lemur/auth/views.py @@ -14,6 +14,7 @@ from flask import Blueprint, current_app from flask.ext.restful import reqparse, Resource, Api from flask.ext.principal import Identity, identity_changed +from lemur.extensions import metrics from lemur.common.utils import get_psuedo_random_string from lemur.users import service as user_service @@ -96,8 +97,10 @@ class Login(Resource): # Tell Flask-Principal the identity changed identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) + metrics.send('successful_login', 'counter', 1) return dict(token=create_token(user)) + metrics.send('invalid_login', 'counter', 1) return dict(message='The supplied credentials are invalid'), 401 @@ -176,6 +179,7 @@ class Ping(Resource): profile = r.json() user = user_service.get_by_email(profile['email']) + metrics.send('successful_login', 'counter', 1) # update their google 'roles' roles = [] @@ -263,6 +267,7 @@ class Google(Resource): user = user_service.get_by_email(profile['email']) if user: + metrics.send('successful_login', 'counter', 1) return dict(token=create_token(user)) diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index b8ca6b6e..6d140d4a 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -11,6 +11,7 @@ from sqlalchemy import func, or_ from flask import g, current_app from lemur import database +from lemur.extensions import metrics from lemur.plugins.base import plugins from lemur.certificates.models import Certificate @@ -271,6 +272,7 @@ def create(**kwargs): cert.notifications = notifications database.update(cert) + metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer)) return cert diff --git a/lemur/extensions.py b/lemur/extensions.py index 47c8d024..d6325be1 100644 --- a/lemur/extensions.py +++ b/lemur/extensions.py @@ -17,3 +17,6 @@ principal = Principal() from flask_mail import Mail smtp_mail = Mail() + +from lemur.metrics import Metrics +metrics = Metrics() diff --git a/lemur/metrics.py b/lemur/metrics.py new file mode 100644 index 00000000..64ddeac3 --- /dev/null +++ b/lemur/metrics.py @@ -0,0 +1,32 @@ +""" +.. module: lemur.metrics + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from flask import current_app +from lemur.plugins.base import plugins + + +class Metrics(object): + """ + :param app: The Flask application object. Defaults to None. + """ + _providers = [] + + def __init__(self, app=None): + if app is not None: + self.init_app(app) + + def init_app(self, app): + """Initializes the application with the extension. + + :param app: The Flask application object. + """ + self._providers = app.config.get('METRIC_PROVIDERS', []) + + def send(self, metric_name, metric_type, metric_value, *args, **kwargs): + for provider in self._providers: + current_app.logger.debug( + "Sending metric '{metric}' to the {provider} provider.".format(metric=metric_name, provider=provider)) + p = plugins.get(provider) + p.submit(metric_name, metric_type, metric_value, *args, **kwargs) diff --git a/lemur/plugins/bases/metric.py b/lemur/plugins/bases/metric.py new file mode 100644 index 00000000..ee9c5ad4 --- /dev/null +++ b/lemur/plugins/bases/metric.py @@ -0,0 +1,16 @@ +""" +.. module: lemur.bases.metric + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +from lemur.plugins.base import Plugin + + +class MetricPlugin(Plugin): + type = 'metric' + + def submit(self, *args, **kwargs): + raise NotImplemented diff --git a/lemur/plugins/lemur_atlas/__init__.py b/lemur/plugins/lemur_atlas/__init__.py new file mode 100644 index 00000000..8ce5a7f3 --- /dev/null +++ b/lemur/plugins/lemur_atlas/__init__.py @@ -0,0 +1,5 @@ +try: + VERSION = __import__('pkg_resources') \ + .get_distribution(__name__).version +except Exception as e: + VERSION = 'unknown' diff --git a/lemur/plugins/lemur_atlas/plugin.py b/lemur/plugins/lemur_atlas/plugin.py new file mode 100644 index 00000000..6b26740f --- /dev/null +++ b/lemur/plugins/lemur_atlas/plugin.py @@ -0,0 +1,107 @@ +""" +.. module: lemur.plugins.lemur_atlas.plugin + :platform: Unix + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. + +.. moduleauthor:: Kevin Glisson +""" +import json +import requests +from requests.exceptions import ConnectionError +from datetime import datetime + +from flask import current_app +from lemur.plugins import lemur_atlas as atlas +from lemur.plugins.bases.metric import MetricPlugin + + +def millis_since_epoch(): + """ + current time since epoch in milliseconds + """ + epoch = datetime.utcfromtimestamp(0) + delta = datetime.now() - epoch + return int(delta.total_seconds() * 1000.0) + + +class AtlasMetricPlugin(MetricPlugin): + title = 'Atlas' + slug = 'atlas-metric' + description = 'Adds support for sending key metrics to Atlas' + version = atlas.VERSION + + author = 'Kevin Glisson' + author_url = 'https://github.com/netflix/lemur' + + options = [ + { + 'name': 'sidecar_host', + 'type': 'str', + 'required': False, + 'help_message': 'If no host is provided localhost is assumed', + 'default': 'localhost' + }, + { + 'name': 'sidecar_port', + 'type': 'int', + 'required': False, + 'default': 8078 + } + ] + + metric_data = {} + sidecar_host = None + sidecar_port = None + + def submit(self, metric_name, metric_type, metric_value, metric_tags=None, options=None): + if not options: + options = self.options + + # TODO marshmallow schema? + valid_types = ['COUNTER', 'GAUGE', 'TIMER'] + if metric_type.upper() not in valid_types: + raise Exception( + "Invalid Metric Type for Atlas: '{metric}' choose from: {options}".format( + metric=metric_type, options=','.join(valid_types) + ) + ) + + if metric_tags: + if not isinstance(metric_tags, dict): + raise Exception( + "Invalid Metric Tags for Atlas: Tags must be in dict format" + ) + + if metric_value == "NaN" or isinstance(metric_value, int) or isinstance(metric_value, float): + self.metric_data['value'] = metric_value + else: + raise Exception( + "Invalid Metric Value for Atlas: Metric must be a number" + ) + + self.metric_data['type'] = metric_type.upper() + self.metric_data['name'] = str(metric_name) + self.metric_data['tags'] = metric_tags + self.metric_data['timestamp'] = millis_since_epoch() + + self.sidecar_host = self.get_option('sidecar_host', options) + self.sidecar_port = self.get_option('sidecar_port', options) + + try: + res = requests.post( + 'http://{host}:{port}/metrics'.format( + host=self.sidecar_host, + port=self.sidecar_port), + data=json.dumps([self.metric_data]) + ) + + if res.status_code != 200: + current_app.logger.warning("Failed to publish altas metric. {0}".format(res.content)) + + except ConnectionError: + current_app.logger.warning( + "AtlasMetrics: could not connect to sidecar at {host}:{port}".format( + host=self.sidecar_host, port=self.sidecar_port + ) + ) diff --git a/setup.py b/setup.py index ff405f03..cf23db95 100644 --- a/setup.py +++ b/setup.py @@ -167,7 +167,8 @@ setup( 'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin', 'java_truststore_export = lemur.plugins.lemur_java.plugin:JavaTruststoreExportPlugin', 'java_keystore_export = lemur.plugins.lemur_java.plugin:JavaKeystoreExportPlugin', - 'openssl_export = lemur.plugins.lemur_openssl.plugin:OpenSSLExportPlugin' + 'openssl_export = lemur.plugins.lemur_openssl.plugin:OpenSSLExportPlugin', + 'atlas_metric = lemur.plugins.lemur_atlas.plugin:AtlasMetricPlugin' ], }, classifiers=[