Exemplo n.º 1
0
class BaseStatusAction(Action):
    """
    Standard base action for status checks. Returns health check and version information.

    If you want to use the status action use `StatusActionFactory(version)`, passing in the version of your service
    and, optionally, the build of your service. If you do not specify an action with name `status` in your server,
    this will be done on your behalf.

    If you want to make a custom status action, subclass this class, make `self._version` return your service's version
    string, `self._build` optionally return your service's build string, and add any additional health check methods
    you desire. Health check methods must start with `check_`.

    Health check methods accept a single argument, the request object (an instance of `ActionRequest`), and return a
    list of tuples in the format `(is_error, code, description)` (or a false-y value if there are no problems):

    - `is_error`: `True` if this is an error, `False` if it is a warning.
    - `code`: Invariant string for this error, like "MYSQL_FAILURE"
    - `description`: Human-readable description of the problem, like "Could not connect to host on port 1234"

    Health check methods can also write to the `self.diagnostics` dictionary to add additional data which will be sent
    back with the response if they like. They are responsible for their own key management in this situation.

    This base status action comes with a disabled-by-default health check method named `_check_client_settings` (the
    leading underscore disables it), which calls `status` on all other services that this service is configured to call
    (using `verbose: False`, which guarantees no further recursive status checking) and includes those responses in
    this action's response. To enable this health check, simply reference it as a new, valid `check_` method name, like
    so:

    .. code:: python

        class MyStatusAction(BaseStatusAction):
            ...
            check_client_settings = BaseStatusAction._check_client_settings
    """
    def __init__(self, *args, **kwargs):
        """
        Constructs a new base status action. Concrete status actions can override this if they want, but must call
        `super`.

        :param settings: The server settings object
        :type settings: dict
        """
        super(BaseStatusAction, self).__init__(*args, **kwargs)

        self.diagnostics = {}

    @abc.abstractproperty
    def _version(self):
        raise NotImplementedError(
            'version must be defined using StatusActionFactory')

    @property
    def _build(self):
        return None

    description = (
        'Returns version info for the service, Python, PySOA, and Conformity. If the service has a build string, that '
        'is also returned. If the service has defined additional health check behavior and the `verbose` request '
        'attribute is not set to `False`, those additional health checks are performed and returned in the '
        '`healthcheck` response attribute. If the `verbose` request attribute is set to `False`, the additional '
        'health checks are not performed and `healthcheck` is not included in the response (importantly, the `check_` '
        'methods are not invoked).')

    request_schema = fields.Nullable(
        fields.Dictionary(
            {
                'verbose':
                fields.Boolean(
                    description=
                    'If specified and False, this instructs the status action to return only the baseline '
                    'status information (Python, service, PySOA, and other library versions) and omit any of '
                    'the health check operations (no `healthcheck` attribute will be included in the '
                    'response). This provides a useful way to obtain the service version very quickly without '
                    'executing the often time-consuming code necessary for the full health check. It defaults '
                    'to True, which means "return everything."', ),
            },
            optional_keys=('verbose', ),
        ))

    response_schema = fields.Dictionary(
        {
            'build':
            fields.UnicodeString(
                description='The version build string, if applicable.'),
            'conformity':
            fields.UnicodeString(
                description='The version of Conformity in use.'),
            'healthcheck':
            fields.Dictionary(
                {
                    'warnings':
                    fields.List(
                        fields.Tuple(
                            fields.UnicodeString(
                                description='The invariant warning code'),
                            fields.UnicodeString(
                                description='The readable warning description'
                            ),
                        ),
                        description=
                        'A list of any warnings encountered during the health checks.',
                    ),
                    'errors':
                    fields.List(
                        fields.Tuple(
                            fields.UnicodeString(
                                description='The invariant error code'),
                            fields.UnicodeString(
                                description='The readable error description'),
                        ),
                        description=
                        'A list of any errors encountered during the health checks.',
                    ),
                    'diagnostics':
                    fields.SchemalessDictionary(
                        key_type=fields.UnicodeString(),
                        description=
                        'A dictionary containing any additional diagnostic information output by the '
                        'health check operations.',
                    ),
                },
                optional_keys=('warnings', 'errors', 'diagnostics'),
                description=
                'Information about any additional health check operations performed.',
            ),
            'pysoa':
            fields.UnicodeString(description='The version of PySOA in use.'),
            'python':
            fields.UnicodeString(description='The version of Python in use.'),
            'version':
            fields.UnicodeString(
                description='The version of the responding service.'),
        },
        optional_keys=(
            'build',
            'healthcheck',
        ),
    )

    def run(self, request):
        """
        Adds version information for Conformity, PySOA, Python, and the service to the response, then scans the class
        for `check_` methods and runs them (unless `verbose` is `False`).

        :param request: The request object
        :type request: EnrichedActionRequest

        :return: The response
        """
        status = {
            'conformity': six.text_type(conformity.__version__),
            'pysoa': six.text_type(pysoa.__version__),
            'python': six.text_type(platform.python_version()),
            'version': self._version,
        }

        if self._build:
            status['build'] = self._build

        if not request.body or request.body.get('verbose', True) is True:
            errors = []
            warnings = []
            self.diagnostics = {}

            # Find all things called "check_<something>" on this class.
            # We can't just scan __dict__ because of class inheritance.
            check_methods = [
                getattr(self, x) for x in dir(self) if x.startswith('check_')
            ]
            for check_method in check_methods:
                # Call the check, and see if it returned anything
                try:
                    problems = check_method(request)
                except TypeError as e:
                    raise RuntimeError(
                        'Status action check_* methods must accept a single argument of type ActionRequest',
                        e,
                    )
                if problems:
                    for is_error, code, description in problems:
                        # Parcel out the values into the right return list
                        if is_error:
                            errors.append((code, description))
                        else:
                            warnings.append((code, description))

            status['healthcheck'] = {
                'errors': errors,
                'warnings': warnings,
                'diagnostics': self.diagnostics,
            }

        return status

    def _check_client_settings(self, request):
        """
        This method checks any client settings configured for this service to call other services, calls the `status`
        action of each configured service with `verbose: False` (which guarantees no further recursive status checking),
        adds that diagnostic information, and reports any problems. To include this check in your status action, define
        `check_client_settings = BaseStatusAction._check_client_settings` in your status action class definition.
        """
        if not request.client.settings:
            # There's no need to even add diagnostic details if no client settings are configured
            return

        self.diagnostics['services'] = {}

        service_names = list(six.iterkeys(request.client.settings))
        try:
            job_responses = request.client.call_jobs_parallel(
                [{
                    'service_name': service_name,
                    'actions': [{
                        'action': 'status',
                        'body': {
                            'verbose': False
                        }
                    }]
                } for service_name in service_names],
                timeout=2,
                catch_transport_errors=True,
                raise_action_errors=False,
                raise_job_errors=False,
            )
        except Exception as e:
            return [(True, 'CHECK_SERVICES_UNKNOWN_ERROR', six.text_type(e))]

        problems = []
        for i, service_name in enumerate(service_names):
            response = job_responses[i]
            if isinstance(response, Exception):
                problems.append((True, '{}_TRANSPORT_ERROR'.format(
                    service_name.upper()), six.text_type(response)), )
            elif response.errors:
                problems.append((True, '{}_CALL_ERROR'.format(
                    service_name.upper()), six.text_type(response.errors)), )
            elif response.actions[0].errors:
                problems.append(
                    (True, '{}_STATUS_ERROR'.format(service_name.upper()),
                     six.text_type(response.actions[0].errors)), )
            else:
                self.diagnostics['services'][service_name] = response.actions[
                    0].body

        return problems
Exemplo n.º 2
0
class RedisTransportSchema(BasicClassSchema):
    contents = {
        'path': fields.UnicodeString(
            description='The path to the Redis client or server transport, in the format `module.name:ClassName`',
        ),
        'kwargs': fields.Dictionary(
            {
                'backend_layer_kwargs': fields.Dictionary(
                    {
                        'connection_kwargs': fields.SchemalessDictionary(
                            description='The arguments used when creating all Redis connections (see Redis-Py docs)',
                        ),
                        'hosts': fields.List(
                            fields.Any(
                                fields.Tuple(fields.UnicodeString(), fields.Integer()),
                                fields.UnicodeString(),
                            ),
                            description='The list of Redis hosts, where each is a tuple of `("address", port)` or the '
                                        'simple string address.',
                        ),
                        'redis_db': fields.Integer(
                            description='The Redis database, a shortcut for putting this in `connection_kwargs`.',
                        ),
                        'redis_port': fields.Integer(
                            description='The port number, a shortcut for putting this on all hosts',
                        ),
                        'sentinel_failover_retries': fields.Integer(
                            description='How many times to retry (with a delay) getting a connection from the Sentinel '
                                        'when a master cannot be found (cluster is in the middle of a failover); '
                                        'should only be used for Sentinel backend type'
                        ),
                        'sentinel_services': fields.List(
                            fields.UnicodeString(),
                            description='A list of Sentinel services (will be discovered by default); should only be '
                                        'used for Sentinel backend type',
                        ),
                    },
                    optional_keys=[
                        'connection_kwargs',
                        'hosts',
                        'redis_db',
                        'redis_port',
                        'sentinel_failover_retries',
                        'sentinel_services',
                    ],
                    allow_extra_keys=False,
                    description='The arguments passed to the Redis connection manager',
                ),
                'backend_type': fields.Constant(
                    *REDIS_BACKEND_TYPES,
                    description='Which backend (standard or sentinel) should be used for this Redis transport'
                ),
                'log_messages_larger_than_bytes': fields.Integer(
                    description='By default, messages larger than 100KB that do not trigger errors (see '
                                '`maximum_message_size_in_bytes`) will be logged with level WARNING to a logger named '
                                '`pysoa.transport.oversized_message`. To disable this behavior, set this setting to '
                                '0. Or, you can set it to some other number to change the threshold that triggers '
                                'logging.',
                ),
                'maximum_message_size_in_bytes': fields.Integer(
                    description='The maximum message size, in bytes, that is permitted to be transmitted over this '
                                'transport (defaults to 100KB on the client and 250KB on the server)',
                ),
                'message_expiry_in_seconds': fields.Integer(
                    description='How long after a message is sent that it is considered expired, dropped from queue',
                ),
                'queue_capacity': fields.Integer(
                    description='The capacity of the message queue to which this transport will send messages',
                ),
                'queue_full_retries': fields.Integer(
                    description='How many times to retry sending a message to a full queue before giving up',
                ),
                'receive_timeout_in_seconds': fields.Integer(
                    description='How long to block waiting on a message to be received',
                ),
                'serializer_config': BasicClassSchema(
                    object_type=BaseSerializer,
                    description='The configuration for the serializer this transport should use',
                ),
            },
            optional_keys=[
                'backend_layer_kwargs',
                'log_messages_larger_than_bytes',
                'maximum_message_size_in_bytes',
                'message_expiry_in_seconds',
                'queue_capacity',
                'queue_full_retries',
                'receive_timeout_in_seconds',
                'serializer_config',
            ],
            allow_extra_keys=False,
        ),
    }

    optional_keys = ()

    description = 'The settings for the Redis transport'
Exemplo n.º 3
0
    'The configuration schema changes slightly based on which config version you specify.',
)
""""""  # Empty docstring to make autodoc document this data


@attr.s
class Configuration(object):
    version = attr.ib()  # type: int
    publishers = attr.ib(
        default=attr.Factory(list))  # type: List[MetricsPublisher]
    error_logger_name = attr.ib(default=None)  # type: Optional[six.text_type]
    enable_meta_metrics = attr.ib(default=False)  # type: bool


@validator.validate_call(
    args=fields.Tuple(copy.deepcopy(CONFIGURATION_SCHEMA)),
    kwargs=None,
    returns=fields.ObjectInstance(Configuration),
)
def create_configuration(
        config_dict):  # type: (Dict[six.text_type, Any]) -> Configuration
    """
    Creates a `Configuration` object using the provided configuration dictionary. Works in similar fashion to logging's
    configuration.

    Expected format of config is a dict:

    .. code-block:: python

        {
            'version': 2,
Exemplo n.º 4
0
class BaseStatusAction(Action):
    """
    Standard base action for status checks.

    Returns heath check and version information.

    If you want to use default StatusAction use StatusActionFactory(version)
    passing in the version of your service.

    If you want to make a custom StatusAction, subclass this class,
    make it get self._version from your service and add additional health check methods.
    Health check methods must start with the word check_.

    Health check methods must take no arguments, and return a list of tuples in the format:
    (is_error, code, description).

    is_error: True if this is an error, False if it is a warning.
    code: Invariant string for this error, like "MYSQL_FAILURE"
    description: Human-readable description of the problem, like "Could not connect to host on port 1234"

    Health check methods can also write to the self.diagnostics dictionary to add additional
    data which will be sent back with the response if they like. They are responsible for their
    own key management in this situation.
    """

    def __init__(self, *args, **kwargs):
        if self.__class__ is BaseStatusAction:
            raise RuntimeError('You cannot use BaseStatusAction directly; it must be subclassed')
        super(BaseStatusAction, self).__init__(*args, **kwargs)

        self.diagnostics = {}

    @property
    def _version(self):
        raise NotImplementedError('version must be defined using StatusActionFactory')

    @property
    def _build(self):
        return None

    request_schema = fields.Dictionary(
        {
            'verbose': fields.Boolean(
                description='If specified and False, this instructs the status action to return only the baseline '
                            'status information (Python, service, PySOA, and other library versions) and omit any of '
                            'the health check operations (no `healthcheck` attribute will be included in the '
                            'response). This provides a useful way to obtain the service version very quickly without '
                            'executing the often time-consuming code necessary for the full health check. It defaults '
                            'to True, which means "return everything."',
            ),
        },
        optional_keys=('verbose', ),
    )

    response_schema = fields.Dictionary(
        {
            'build': fields.UnicodeString(),
            'conformity': fields.UnicodeString(),
            'healthcheck': fields.Dictionary(
                {
                    'warnings': fields.List(fields.Tuple(fields.UnicodeString(), fields.UnicodeString())),
                    'errors': fields.List(fields.Tuple(fields.UnicodeString(), fields.UnicodeString())),
                    'diagnostics': fields.SchemalessDictionary(key_type=fields.UnicodeString()),
                },
                optional_keys=('warnings', 'errors', 'diagnostics'),
            ),
            'pysoa': fields.UnicodeString(),
            'python': fields.UnicodeString(),
            'version': fields.UnicodeString(),
        },
        optional_keys=('build', 'healthcheck', ),
    )

    def run(self, request):
        """
        Scans the class for check_ methods and runs them.
        """
        status = {
            'conformity': six.text_type(conformity.__version__),
            'pysoa': six.text_type(pysoa.__version__),
            'python': six.text_type(platform.python_version()),
            'version': self._version,
        }

        if self._build:
            status['build'] = self._build

        if request.body.get('verbose', True) is True:
            errors = []
            warnings = []
            self.diagnostics = {}

            # Find all things called "check_<something>" on this class.
            # We can't just scan __dict__ because of class inheritance.
            check_methods = [getattr(self, x) for x in dir(self) if x.startswith('check_')]
            for check_method in check_methods:
                # Call the check, and see if it returned anything
                problems = check_method()
                if problems:
                    for is_error, code, description in problems:
                        # Parcel out the values into the right return list
                        if is_error:
                            errors.append((code, description))
                        else:
                            warnings.append((code, description))

            status['healthcheck'] = {
                'errors': errors,
                'warnings': warnings,
                'diagnostics': self.diagnostics,
            }

        return status
Exemplo n.º 5
0
class RedisTransportSchema(BasicClassSchema):
    contents = {
        'path': fields.UnicodeString(),
        'kwargs': fields.Dictionary(
            {
                'backend_layer_kwargs': fields.Dictionary(
                    {
                        'connection_kwargs': fields.SchemalessDictionary(
                            description='The arguments used when creating all Redis connections (see Redis-Py docs)',
                        ),
                        'hosts': fields.List(
                            fields.Any(
                                fields.Tuple(fields.UnicodeString(), fields.Integer()),
                                fields.UnicodeString(),
                            ),
                            description='The list of Redis hosts',
                        ),
                        'redis_db': fields.Integer(
                            description='The Redis database, a shortcut for putting this in `connection_kwargs`.',
                        ),
                        'redis_port': fields.Integer(
                            description='The port number, a shortcut for putting this on all hosts',
                        ),
                        'sentinel_failover_retries': fields.Integer(
                            description='How many times to retry (with a delay) getting a connection from the Sentinel '
                                        'when a master cannot be found (cluster is in the middle of a failover); '
                                        'should only be used for Sentinel backend type'
                        ),
                        'sentinel_refresh_interval': fields.Integer(
                            description='Deprecated; unused; to be removed before final release.',
                        ),
                        'sentinel_services': fields.List(
                            fields.UnicodeString(),
                            description='A list of Sentinel services (will be discovered by default); should only be '
                                        'used for Sentinel backend type',
                        ),
                    },
                    optional_keys=[
                        'connection_kwargs',
                        'hosts',
                        'redis_db',
                        'redis_port',
                        'sentinel_failover_retries',
                        'sentinel_refresh_interval',
                        'sentinel_services',
                    ],
                    allow_extra_keys=False,
                    description='The arguments passed to the Redis connection manager',
                ),
                'backend_type': fields.Constant(
                    *REDIS_BACKEND_TYPES,
                    description='Which backend (standard or sentinel) should be used for this Redis transport'
                ),
                'message_expiry_in_seconds': fields.Integer(
                    description='How long after a message is sent that it is considered expired, dropped from queue',
                ),
                'queue_capacity': fields.Integer(
                    description='The capacity of the message queue to which this transport will send messages',
                ),
                'queue_full_retries': fields.Integer(
                    description='How many times to retry sending a message to a full queue before giving up',
                ),
                'receive_timeout_in_seconds': fields.Integer(
                    description='How long to block waiting on a message to be received',
                ),
                'serializer_config': BasicClassSchema(
                    object_type=BaseSerializer,
                    description='The configuration for the serializer this transport should use',
                ),
            },
            optional_keys=[
                'backend_layer_kwargs',
                'message_expiry_in_seconds',
                'queue_capacity',
                'queue_full_retries',
                'receive_timeout_in_seconds',
                'serializer_config',
            ],
            allow_extra_keys=False,
        ),
    }