class HttpEndpoint(IOpenable, IConfigurable, IReferenceable): """ Used for creating HTTP endpoints. An endpoint is a URL, at which a given service can be accessed by a client. ### Configuration parameters ### Parameters to pass to the [[configure]] method for component configuration: - connection(s) - the connection resolver's connections; - "connection.discovery_key" - the key to use for connection resolving in a discovery service; - "connection.protocol" - the connection's protocol; - "connection.host" - the target host; - "connection.port" - the target port; - "connection.uri" - the target URI. ### References ### A logger, counters, and a connection resolver can be referenced by passing the following references to the object's [[setReferences]] method: - *:logger:*:*:1.0 (optional) ILogger components to pass log messages - *:counters:*:*:1.0 (optional) ICounters components to pass collected measurements - *:discovery:*:*:1.0 (optional) IDiscovery services to resolve connection Example: def my_method(_config, _references): endpoint = HttpEndpoint() if (_config) endpoint.configure(_config) if (_references) endpoint.setReferences(_references) ... endpoint.open(correlationId) ... """ _default_config = None _connection_resolver = None _logger = None _counters = None _registrations = None _service = None _server = None _debug = False _uri = None _file_max_size = 200 * 1024 * 1024 _maintenance_enabled = False _protocol_upgrade_enabled = False def __init__(self): """ Creates HttpEndpoint """ self._default_config = ConfigParams.from_tuples( "connection.protocol", "http", "connection.host", "0.0.0.0", "connection.port", 3000, "credential.ssl_key_file", None, "credential.ssl_crt_file", None, "credential.ssl_ca_file", None, "options.maintenance_enabled", False, "options.request_max_size", 1024 * 1024, "options.file_max_size", 200 * 1024 * 1024, "connection.connect_timeout", 60000, "connection.debug", True) self._connection_resolver = HttpConnectionResolver() self._logger = CompositeLogger() self._counters = CompositeCounters() self._registrations = [] def configure(self, config): """ Configures this HttpEndpoint using the given configuration parameters. - connection(s) - the connection resolver's connections; - "connection.discovery_key" - the key to use for connection resolving in a discovery service; - "connection.protocol" - the connection's protocol; - "connection.host" - the target host; - "connection.port" - the target port; - "connection.uri" - the target URI. :param config: configuration parameters, containing a "connection(s)" section. """ config = config.set_defaults(self._default_config) self._connection_resolver.configure(config) self._file_max_size = config.get_as_boolean_with_default( 'options.file_max_size', self._file_max_size) self._maintenance_enabled = config.get_as_long_with_default( 'options.maintenance_enabled', self._maintenance_enabled) self._protocol_upgrade_enabled = config.get_as_boolean_with_default( 'options.protocol_upgrade_enabled', self._protocol_upgrade_enabled) self._debug = config.get_as_boolean_with_default( 'connection.debug', self._debug) def set_references(self, references): """ Sets references to this endpoint's logger, counters, and connection resolver. - *:logger:*:*:1.0 (optional) ILogger components to pass log messages - *:counters:*:*:1.0 (optional) ICounters components to pass collected measurements - *:discovery:*:*:1.0 (optional) IDiscovery services to resolve connection :param references: an IReferences object, containing references to a logger, counters, and a connection resolver. """ self._logger.set_references(references) self._counters.set_references(references) self._connection_resolver.set_references(references) def is_opened(self): """ Checks if the component is opened. :return: whether or not this endpoint is open with an actively listening REST server. """ return not (self._server is None) def open(self, correlation_id): """ Opens a connection using the parameters resolved by the referenced connection resolver and creates a REST server (service) using the set options and parameters. :param correlation_id: (optional) transaction id to trace execution through call chain. """ if self.is_opened(): return connection = self._connection_resolver.resolve(correlation_id) if connection is None: raise ConfigException(correlation_id, "NO_CONNECTION", "Connection for REST client is not defined") self._uri = connection.get_uri() # verify https with bottle certfile = None keyfile = None if connection.get_protocol('http') == 'https': certfile = connection.get_as_nullable_string('ssl_crt_file') keyfile = connection.get_as_nullable_string('ssl_key_file') # Create instance of bottle application self._service = SessionMiddleware( bottle.Bottle(catchall=True, autojson=True)).app self._service.config['catchall'] = True self._service.config['autojson'] = True # Enable CORS requests self._service.add_hook('after_request', self._enable_cors) self._service.route('/', 'OPTIONS', self._options_handler) self._service.route('/<path:path>', 'OPTIONS', self._options_handler) self._service.add_hook('after_request', self._do_maintance) self._service.add_hook('after_request', self._no_cache) self._service.add_hook('before_request', self._add_compatibility) # Register routes # self.perform_registrations() def start_server(): self._service.run(server=self._server, debug=self._debug) # self.perform_registrations() host = connection.get_host() port = connection.get_port() # Starting service try: self._server = SSLCherryPyServer(host=host, port=port, certfile=certfile, keyfile=keyfile) # Start server in thread Thread(target=start_server).start() # Give 2 sec for initialization self._connection_resolver.register(correlation_id) self._logger.debug( correlation_id, f"Opened REST service at {self._uri}", ) self.perform_registrations() except Exception as ex: self._server = None raise ConnectionException(correlation_id, 'CANNOT_CONNECT', 'Opening REST service failed') \ .wrap(ex).with_details('url', self._uri) def close(self, correlation_id): """ Closes this endpoint and the REST server (service) that was opened earlier. :param correlation_id: (optional) transaction id to trace execution through call chain. """ try: if not (self._server is None): self._server.shutdown() self._service.close() self._logger.debug(correlation_id, "Closed REST service at %s", self._uri) self._server = None self._service = None self._uri = None except Exception as ex: self._logger.warn(correlation_id, "Failed while closing REST service: " + str(ex)) def register(self, registration): """ Registers a registerable object for dynamic endpoint discovery. :param registration: the registration to add. """ self._registrations.append(registration) def unregister(self, registration): """ Unregisters a registerable object, so that it is no longer used in dynamic endpoint discovery. :param registration: the registration to remove. """ self._registrations.remove(registration) def perform_registrations(self): for registration in self._registrations: registration.register() def fix_route(self, route) -> str: if route is not None and len(route) > 0: if route[0] != '/': route = f'/{route}' return route return '' def register_route(self, method, route, schema, handler): """ Registers an action in this objects REST server (service) by the given method and route. :param method: the HTTP method of the route. :param route: the route to register in this object's REST server (service). :param schema: the schema to use for parameter validation. :param handler: the action to perform at the given route. """ method = method.upper() # if method == 'DELETE': # method = 'DEL' route = self.fix_route(route) def wrapper(*args, **kwargs): try: if isinstance(schema, Schema): params = self.get_data() correlation_id = params[ 'correlation_id'] if 'correlation_id' in params else None error = schema.validate_and_throw_exception( correlation_id, params, False) return handler(*args, **kwargs) except Exception as ex: return HttpResponseSender.send_error(ex) self._service.route(route, method, wrapper) def get_data(self): if request.json: return request.json else: return None def _enable_cors(self): response.headers['Access-Control-Max-Age'] = '5' response.headers['Access-Control-Allow-Origin'] = '*' response.headers[ 'Access-Control-Allow-Methods'] = 'PUT, GET, POST, DELETE, OPTIONS' response.headers[ 'Access-Control-Allow-Headers'] = 'Authorization, Origin, Accept, Content-Type, X-Requested-With' def _do_maintance(self): """ :return: maintenance error code """ # Make this more sophisticated if self._maintenance_enabled: response.headers['Retry-After'] = 3600 response.status = 503 def _no_cache(self): """ Prevents IE from caching REST requests """ response.headers[ 'Cache-Control'] = 'no-cache, no-store, must-revalidate' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = 0 def _add_compatibility(self): def inner(name): if request.query: param = request.query[name] if param: return param if request.body: param = request.json[name] if param: return param if request.params: param = request.params[name] if param: return param return None request['param'] = inner request['route'] = {'params': request.params} def _options_handler(self, ath=None): return def get_param(self, param, default=None): return request.params.get(param, default) def get_correlation_id(self): return request.query.get('correlation_id') def register_route_with_auth(self, method, route, schema, authorize, action): """ Registers an action with authorization in this objects REST server (service) by the given method and route. :param method: the HTTP method of the route. :param route: the route to register in this object's REST server (service). :param schema: the schema to use for parameter validation. :param authorize: the authorization interceptor :param action: the action to perform at the given route. """ if authorize: next_action = action action = lambda req, res: authorize( request, response, next_action(response, response)) self.register_route(method, route, schema, action) def register_interceptor(self, route, action): """ Registers a middleware action for the given route. :param route: the route to register in this object's REST server (service). :param action: the middleware action to perform at the given route. """ route = self.fix_route(route) self._service.add_hook( 'before_request', lambda: action(request, response) if not (route is not None and route != '' and request.url. startswith(route)) else None)
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")