def test_resolve_parameters(self):
     connection_resolver = HttpConnectionResolver()
     connection_resolver.configure(
         ConfigParams.from_tuples("connection.protocol", "http",
                                  "connection.host", "somewhere.com",
                                  "connection.port", 777))
     connection = connection_resolver.resolve(None)
     assert connection.get_as_string('uri') == "http://somewhere.com:777"
예제 #2
0
    def test_connection_uri(self):
        connection_resolver = HttpConnectionResolver()
        connection_resolver.configure(ConfigParams.from_tuples("connection.uri", "https://somewhere.com:123"))

        connection = connection_resolver.resolve(None)

        assert connection.get_protocol() == "https"
        assert connection.get_host() == "somewhere.com"
        assert connection.get_port() == 123
        assert connection.get_uri() == "https://somewhere.com:123"
    def test_resolve_uri(self):
        connection_resolver = HttpConnectionResolver()
        connection_resolver.configure(
            ConfigParams.from_tuples("connection.uri",
                                     "http://somewhere.com:777"))
        connection = connection_resolver.resolve(None)

        assert connection.get_as_string('protocol') == "http"
        assert connection.get_as_string('host') == "somewhere.com"
        assert connection.get_as_integer('port') == 777
        assert connection.get_as_string('uri') == "http://somewhere.com:777"
예제 #4
0
    def test_https_with_no_credentials_connection_params(self):
        connection_resolver = HttpConnectionResolver()
        connection_resolver.configure(ConfigParams.from_tuples(
            "connection.host", "somewhere.com",
            "connection.port", 123,
            "connection.protocol", "https",
            "credential.internal_network", "internal_network"
        ))
        connection = connection_resolver.resolve(None)

        assert 'https' == connection.get_protocol()
        assert 'somewhere.com' == connection.get_host()
        assert 123 == connection.get_port()
        assert 'https://somewhere.com:123' == connection.get_uri()
        assert connection.get('credential.internal_network')
    def test_https_with_no_credentials_connection_params(self):
        connection_resolver = HttpConnectionResolver()
        connection_resolver.configure(
            ConfigParams.from_tuples("connection.host", "somewhere.com",
                                     "connection.port", 123,
                                     "connection.protocol", "https",
                                     "credential.internal_network",
                                     "internal_network"))
        connection = connection_resolver.resolve(None)

        assert 'https' == connection.get_as_string('protocol')
        assert 'somewhere.com' == connection.get_as_string('host')
        assert 123 == connection.get_as_integer('port')
        assert 'https://somewhere.com:123' == connection.get_as_string('uri')
        assert connection.get_as_nullable_string('internal_network') is None
예제 #6
0
    def test_https_with_credentials_connection_params(self):
        connection_resolver = HttpConnectionResolver()
        connection_resolver.configure(ConfigParams.from_tuples(
            "connection.host", "somewhere.com",
            "connection.port", 123,
            "connection.protocol", "https",
            "credential.ssl_key_file", "ssl_key_file",
            "credential.ssl_crt_file", "ssl_crt_file",
        ))

        connection = connection_resolver.resolve(None)

        assert 'https' == connection.get_protocol()
        assert 'somewhere.com' == connection.get_host()
        assert 123 == connection.get_port()
        assert 'https://somewhere.com:123' == connection.get_uri()
        assert 'ssl_key_file' == connection.get('credential.ssl_key_file')
        assert 'ssl_crt_file' == connection.get('credential.ssl_crt_file')
class ElasticSearchLogger(CachedLogger, IReferenceable, IOpenable):
    """
    Logger that dumps execution logs to ElasticSearch service.

    ElasticSearch is a popular search index. It is often used
    to store and index execution logs by itself or as a part of
    ELK (ElasticSearch - Logstash - Kibana) stack.

    Authentication is not supported in this version.

    ### Configuration parameters ###
        - level:             maximum log level to capture
        - source:            source (context) name
        - connection(s):
            - discovery_key:         (optional) a key to retrieve the connection from :class:`IDiscovery <pip_services3_components.connect.IDiscovery.IDiscovery>`
            - protocol:              connection protocol: http or https
            - host:                  host name or IP address
            - port:                  port number
            - uri:                   resource URI or connection string with all parameters in it
        - options:
            - interval:        interval in milliseconds to save log messages (default: 10 seconds)
            - max_cache_size:  maximum number of messages stored in this cache (default: 100)
            - index:           ElasticSearch index name (default: "log")
            - date_format      The date format to use when creating the index name. Eg. log-YYYYMMDD (default: "YYYYMMDD").
            - daily:           True to create a new index every day by adding date suffix to the index name (default: False)
            - reconnect:       reconnect timeout in milliseconds (default: 60 sec)
            - timeout:         invocation timeout in milliseconds (default: 30 sec)
            - max_retries:     maximum number of retries (default: 3)
            - index_message:   True to enable indexing for message object (default: False)
            - include_type_name: Will create using a "typed" index compatible with ElasticSearch 6.x (default: false)

    ### References ###
        - `*:context-info:*:*:1.0`    (optional) :class:`ContextInfo <pip_services3_components.info.ContextInfo.ContextInfo>` to detect the context id and specify counters source
        - `*:discovery:*:*:1.0`       (optional) :class:`IDiscovery <pip_services3_components.connect.IDiscovery.IDiscovery>` services to resolve connection

    Example:

    .. code-block:: python

        logger = new ElasticSearchLogger()
        logger.configure(ConfigParams.from_tuples(
            "connection.protocol", "http",
            "connection.host", "localhost",
            "connection.port", 9200
        ))

        try:
            logger.open("123")
        except Exception as err:
            logger.error("123", err, "Error occured: {}", err.message)
            # do something

        logger.debug("123", "Everything is OK.");
    """
    def __init__(self):
        """
        Creates a new instance of the logger.
        """
        super(ElasticSearchLogger, self).__init__()

        self.__connection_resolver = HttpConnectionResolver()

        self.__timer = None
        self.__index = 'log'
        self._date_format = 'YYYYMMDD'
        self.__daily_index = False
        self.__current_index: str = None
        self.__reconnect = 60000
        self.__timeout = 30000
        self.__max_retries = 3
        self.__index_message = False
        self.__include_type_name = False

        self.__client = None

    def configure(self, config: ConfigParams):
        """
        Configures component by passing configuration parameters.

        :param config: configuration parameters to be set.
        """
        super().configure(config)

        self.__connection_resolver.configure(config)

        self.__index = config.get_as_string_with_default('index', self.__index)
        self._date_format = config.get_as_string_with_default(
            'date_format', self._date_format)
        self.__daily_index = config.get_as_boolean_with_default(
            'daily', self.__daily_index)
        self.__reconnect = config.get_as_integer_with_default(
            'options.reconnect', self.__reconnect)
        self.__timeout = config.get_as_integer_with_default(
            'options.timeout', self.__timeout)
        self.__max_retries = config.get_as_integer_with_default(
            'options.max_retries', self.__max_retries)
        self.__index_message = config.get_as_boolean_with_default(
            'options.index_message', self.__index_message)
        self.__include_type_name = config.get_as_boolean_with_default(
            'options.include_type_name', self.__include_type_name)

    def set_references(self, references: IReferences):
        """
        Sets references to dependent components.

        :param references: references to locate the component dependencies.
        """
        super().set_references(references)
        self.__connection_resolver.set_references(references)

    def is_open(self) -> bool:
        """
        Checks if the component is opened.

        :return: True if the component has been opened and False otherwise.
        """
        return self.__timer is not None

    def open(self, correlation_id: Optional[str]):
        """
        Opens the component.

        :param correlation_id: (optional) transaction id to trace execution through call chain.
        """
        if self.is_open():
            return

        connection = self.__connection_resolver.resolve(correlation_id)
        if connection is None:
            raise ConfigException(correlation_id, 'NO_CONNECTION',
                                  'Connection is not configured')
        uri = connection.get_as_string('uri')

        options = {
            'request_timeout': self.__timeout,
            'dead_timeout': self.__reconnect,
            'max_retries': self.__max_retries
        }

        self.__client = Elasticsearch(hosts=[uri], kwargs=options)
        try:
            self.__create_index_if_needed(correlation_id, True)
            self.__timer = SetInterval(self.dump, self._interval)
            self.__timer.start()
        except Exception as err:
            raise err

    def close(self, correlation_id: Optional[str]):
        """
        Closes component and frees used resources.

        :param correlation_id: (optional) transaction id to trace execution through call chain.
        """
        try:
            self._save(self._cache)
            self.__client.close()

            if self.__timer:
                self.__timer.stop()
            self._cache = []
            self.__timer = None
            self.__client = None

        except Exception as err:
            raise err

    def __get_current_index(self) -> str:
        if not self.__daily_index: return self.__index

        today = datetime.utcnow().astimezone(
            tz=timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT")
        date_pattern = Moment(today).format(self._date_format)

        return self.__index + '-' + date_pattern

    def __create_index_if_needed(self, correlation_id: Optional[str], force):
        new_index = self.__get_current_index()
        if not force and self.__current_index == new_index:
            return

        self.__current_index = new_index

        try:
            if not self.__client.indices.exists(index=self.__current_index):
                self.__client.indices.create(
                    index=self.__current_index,
                    include_type_name=self.__include_type_name,
                    body={
                        'settings': {
                            'number_of_shards': 1
                        },
                        'mappings': self._get_index_schema()
                    })
        except Exception as err:
            # Skip already exist errors
            if 'resource_already_exists' in str(err):
                return
            raise err

    def _get_index_schema(self) -> dict:
        schema = {
            'properties': {
                'time': {
                    'type': 'date',
                    'index': True
                },
                'source': {
                    'type': "keyword",
                    'index': True
                },
                'level': {
                    'type': "keyword",
                    'index': True
                },
                'correlation_id': {
                    'type': "text",
                    'index': True
                },
                'error': {
                    'type': 'object',
                    'properties': {
                        'type': {
                            'type': "keyword",
                            'index': True
                        },
                        'category': {
                            'type': "keyword",
                            'index': True
                        },
                        'status': {
                            'type': "integer",
                            'index': False
                        },
                        'code': {
                            'type': "keyword",
                            'index': True
                        },
                        'message': {
                            'type': "text",
                            'index': False
                        },
                        'details': {
                            'type': "object"
                        },
                        'correlation_id': {
                            'type': "text",
                            'index': False
                        },
                        'cause': {
                            'type': "text",
                            'index': False
                        },
                        'stack_trace': {
                            'type': "text",
                            'index': False
                        }
                    }
                },
                'message': {
                    'type': 'text',
                    'index': self.__index_message
                }
            }
        }

        if self.__include_type_name:
            return {'log_message': schema}
        else:
            return schema

    def _save(self, messages: List[LogMessage]):
        """
        Saves log messages from the cache.

        :param messages:  a list with log messages
        """
        if not self.is_open() and len(messages) == 0:
            return
        try:
            self.__create_index_if_needed('elasticsearch_logger', False)
            bulk = []
            for message in messages:
                data = {'_source': self.__error_to_json(message)}
                data.update(self.__get_log_item())
                bulk.append(data)

            if bulk:
                response = helpers.bulk(self.__client, bulk, stats_only=True)
                if response[0] != len(bulk):
                    raise Exception('Not all messages were recorded.')

        except Exception as err:
            raise err

    def __get_log_item(self) -> Any:
        if self.__include_type_name:
            return {
                '_index': self.__current_index,
                '_type': "log_message",
                '_id': IdGenerator.next_long()
            }  # ElasticSearch 6.x
        else:
            return {
                '_index': self.__current_index,
                '_id': IdGenerator.next_long()
            }  # ElasticSearch 7.x

    def __error_to_json(self, err):
        # Convert objects for json serialization
        # TODO: Maybe need move this to other module

        result_dict = {}

        if err is None:
            return None
        elif isinstance(err, dict):
            items = err.items()
        elif isinstance(err, Iterable):
            items = err
        else:
            items = inspect.getmembers(err)

        for key, value in items:
            if not key.startswith('_') and not callable(value):
                if key == 'args':
                    return ", ".join(value)
                if not isinstance(value,
                                  (int, float, str, tuple, list, datetime)):
                    result_dict[key] = self.__error_to_json(value)
                else:
                    result_dict[key] = value if not isinstance(
                        value, (tuple, list)) else ", ".join(value)

        return result_dict
예제 #8
0
    def test_https_with_missing_credentials_connection_params(self):
        # Section missing
        connection_resolver = HttpConnectionResolver()
        connection_resolver.configure(ConfigParams.from_tuples(
            "connection.host", "somewhere.com",
            "connection.port", 123,
            "connection.protocol", "https"
        ))
        print('Test - section missing')
        try:
            connection_resolver.resolve(None)
        except ConfigException as err:
            assert err.code == 'NO_SSL_KEY_FILE'
            assert err.name == 'NO_SSL_KEY_FILE'
            assert err.message == 'SSL key file is not configured in credentials'
            assert err.category == 'Misconfiguration'

        # ssl_crt_file missing
        connection_resolver = HttpConnectionResolver()
        connection_resolver.configure(ConfigParams.from_tuples(
            "connection.host", "somewhere.com",
            "connection.port", 123,
            "connection.protocol", "https",
            "credential.ssl_key_file", "ssl_key_file"
        ))

        print('Test - ssl_crt_file missing')
        try:
            connection_resolver.resolve(None)
        except ConfigException as err:
            assert err.code == 'NO_SSL_CRT_FILE'
            assert err.name == 'NO_SSL_CRT_FILE'
            assert err.message == 'SSL crt file is not configured in credentials'
            assert err.category == 'Misconfiguration'

        # ssl_key_file missing
        connection_resolver = HttpConnectionResolver()
        connection_resolver.configure(ConfigParams.from_tuples(
            "connection.host", "somewhere.com",
            "connection.port", 123,
            "connection.protocol", "https",
            "credential.ssl_crt_file", "ssl_crt_file"
        ))
        print('Test - ssl_key_file missing')
        try:
            connection_resolver.resolve(None)
        except ConfigException as err:
            assert err.code == 'NO_SSL_KEY_FILE'
            assert err.name == 'NO_SSL_KEY_FILE'
            assert err.message == 'SSL key file is not configured in credentials'
            assert err.category == 'Misconfiguration'

        # ssl_key_file, ssl_crt_file present
        connection_resolver = HttpConnectionResolver()
        connection_resolver.configure(ConfigParams.from_tuples(
            "connection.host", "somewhere.com",
            "connection.port", 123,
            "connection.protocol", "https",
            "credential.ssl_key_file", "ssl_key_file",
            "credential.ssl_crt_file", "ssl_crt_file"
        ))
        print('Test - ssl_key_file,  ssl_crt_file present')
        connection = connection_resolver.resolve(None)

        assert 'https' == connection.get_protocol()
        assert 'somewhere.com' == connection.get_host()
        assert 123 == connection.get_port()
        assert 'https://somewhere.com:123' == connection.get_uri()
        assert 'ssl_key_file' == connection.get('credential.ssl_key_file')
        assert 'ssl_crt_file' == connection.get('credential.ssl_crt_file')
class PrometheusCounters(CachedCounters, IReferenceable, IOpenable):
    """
    Performance counters that send their metrics to Prometheus service.

    The component is normally used in passive mode conjunction with :class:`PrometheusMetricsService <pip_services3_prometheus.services.PrometheusMetricsService.PrometheusMetricsService>`.
    Alternatively when connection parameters are set it can push metrics to Prometheus PushGateway.

    ### Configuration parameters ###
        - connection(s):
          - discovery_key:         (optional) a key to retrieve the connection from :class:`IDiscovery <pip_services3_components.connect.IDiscovery.IDiscovery>`
          - protocol:              connection protocol: http or https
          - host:                  host name or IP address
          - port:                  port number
          - uri:                   resource URI or connection string with all parameters in it
        - options:
          - retries:               number of retries (default: 3)
          - connect_timeout:       connection timeout in milliseconds (default: 10 sec)
          - timeout:               invocation timeout in milliseconds (default: 10 sec)

    ### References ###
        - `*:logger:*:*:1.0`           (optional) :class:`ILogger <pip_services3_components.log.ILogger.ILogger>` components to pass log messages
        - `*:counters:*:*:1.0`         (optional) :class:`ICounters <pip_services3_components.count.ICounters.ICounters>` components to pass collected measurements
        - `*:discovery:*:*:1.0`        (optional) :class:`IDiscovery <pip_services3_components.connect.IDiscovery.IDiscovery>` services to resolve connection

    See :class:`RestService <pip_services3_rpc.services.RestService.RestService>`, :class:`CommandableHttpService <pip_services3_rpc.services.CommandableHttpService.CommandableHttpService>`,

    Example:

    .. code-block:: python

        counters = PrometheusCounters()
        counters.configure(ConfigParams.from_tuples(
            "connection.protocol", "http",
            "connection.host", "localhost",
            "connection.port", 8080
        ))

        counters.open("123")

        counters.increment("mycomponent.mymethod.calls")
        timing = counters.begin_timing("mycomponent.mymethod.exec_time")
        try:
            ...
        finally:
            timing.end_timing()

        counters.dump()
    """
    def __init__(self):
        """
        Creates a new instance of the performance counters.
        """
        super(PrometheusCounters, self).__init__()
        self.__logger = CompositeLogger()
        self.__connection_resolver = HttpConnectionResolver()
        self.__opened = False
        self.__source: str = None
        self.__instance: str = None
        self.__push_enabled: bool = None
        self.__client: Any = None
        self.__request_route: str = None

    def configure(self, config: ConfigParams):
        """
        Configures component by passing configuration parameters.

        :param config: configuration parameters to be set.
        """
        super().configure(config)

        self.__connection_resolver.configure(config)
        self.__source = config.get_as_float_with_default(
            'source', self.__source)
        self.__instance = config.get_as_float_with_default(
            'instance', self.__instance)
        self.__push_enabled = config.get_as_float_with_default(
            'push_enabled', True)

    def set_references(self, references: IReferences):
        """
        Sets references to dependent components.

        :param references: references to locate the component dependencies.
        """
        self.__logger.set_references(references)
        self.__connection_resolver.set_references(references)

        context_info = references.get_one_optional(
            Descriptor("pip-services", "context-info", "default", "*", "1.0"))
        if context_info is not None and self.__source is None:
            self.__source = context_info.name
        if context_info is not None and self.__instance is None:
            self.__instance = context_info.context_id

    def is_open(self) -> bool:
        """
        Checks if the component is opened.

        :return: true if the component has been opened and false otherwise.
        """
        return self.__opened

    def open(self, correlation_id: Optional[str]):
        """
        Opens the component.

        :param correlation_id: (optional) transaction id to trace execution through call chain.
        """
        if self.__opened or not self.__push_enabled:
            return

        self.__opened = True

        try:
            connection = self.__connection_resolver.resolve(correlation_id)

            job = self.__source or 'unknown'
            instance = self.__instance or socket.gethostname()
            self.__request_route = "/metrics/job/" + job + "/instance/" + instance
            uri = connection.get_as_string('uri').split('://')[-1]
            if connection.get_as_string('protocol') == 'https':
                self.__client = HTTPSConnectionPool(uri)
            else:
                self.__client = HTTPConnectionPool(uri)

        except Exception as err:
            self.__client = None
            self.__logger.warn(
                correlation_id,
                "Connection to Prometheus server is not configured: " +
                str(err))

    def close(self, correlation_id: Optional[str]):
        """
        Closes component and frees used resources.

        :param correlation_id: (optional) transaction id to trace execution through call chain.
        """
        self.__opened = False
        self.__request_route = None
        try:
            if self.__client:
                self.__client.close()
        finally:
            self.__client = None

    def _save(self, counters: List[Counter]):
        """
        Saves the current counters measurements.

        :param counters: current counters measurements to be saves.
        """
        if self.__client is None or not self.__push_enabled: return

        body = PrometheusCounterConverter.to_string(counters, None, None)
        err = None
        response = None
        try:
            response = self.__client.request('PUT',
                                             self.__request_route,
                                             body=body)
        except Exception as ex:
            err = ex
        finally:
            if err or response.status >= 400:
                self.__logger.error("prometheus-counters", err,
                                    "Failed to push metrics to prometheus")