def test_track_request_works_as_expected(self):
        def process(data, context):
            data.properties["NEW_PROP"] = "MYPROP"
            context.user.id = "BOTUSER"
            context.session.id = "BOTSESSION"
            return True

        sender = MockTelemetrySender()
        queue = channel.SynchronousQueue(sender)
        client = TelemetryClient(channel.TelemetryChannel(context=None, queue=queue))
        client.add_telemetry_processor(process)
        client.context.instrumentation_key = '99999999-9999-9999-9999-999999999999'
        client.context.device = None
        client.track_request('test', 'http://tempuri.org', True, 'START_TIME', 13, 42, 'OPTIONS', { 'foo': 'bar' }, { 'x': 42 }, 'ID_PLACEHOLDER')
        client.flush()
        expected = '{"ver": 1, "name": "Microsoft.ApplicationInsights.Request", "time": "TIME_PLACEHOLDER", "sampleRate": 100.0, "iKey": "99999999-9999-9999-9999-999999999999", "tags": {"ai.device.id": "DEVICE_ID_PLACEHOLDER", "ai.device.locale": "DEVICE_LOCALE_PLACEHOLDER", "ai.device.osVersion": "DEVICE_OS_VERSION_PLACEHOLDER", "ai.device.type": "DEVICE_TYPE_PLACEHOLDER", "ai.internal.sdkVersion": "SDK_VERSION_PLACEHOLDER", "ai.session.id": "BOTSESSION", "ai.user.id": "BOTUSER"}, "data": {"baseType": "RequestData", "baseData": {"ver": 2, "id": "ID_PLACEHOLDER", "name": "test", "duration": "00:00:00.013", "responseCode": "42", "success": true, "url": "http://tempuri.org", "properties": {"NEW_PROP": "MYPROP", "foo": "bar"}, "measurements": {"x": 42}}}}'
        sender.data.time = 'TIME_PLACEHOLDER'
        sender.data.tags['ai.internal.sdkVersion'] = 'SDK_VERSION_PLACEHOLDER'
        sender.data.tags['ai.device.id'] = "DEVICE_ID_PLACEHOLDER"
        sender.data.tags['ai.device.locale'] = "DEVICE_LOCALE_PLACEHOLDER"
        sender.data.tags['ai.device.osVersion'] = "DEVICE_OS_VERSION_PLACEHOLDER"
        sender.data.tags['ai.device.type'] = "DEVICE_TYPE_PLACEHOLDER"
        actual = json.dumps(sender.data.write())
        self.maxDiff = None
        self.assertEqual(expected, actual)
    def test_track_request_works_as_expected(self):
        def process(data, context):
            data.properties["NEW_PROP"] = "MYPROP"
            context.user.id = "BOTUSER"
            context.session.id = "BOTSESSION"
            return True

        sender = MockTelemetrySender()
        queue = channel.SynchronousQueue(sender)
        client = TelemetryClient(
            channel.TelemetryChannel(context=None, queue=queue))
        client.add_telemetry_processor(process)
        client.context.instrumentation_key = '99999999-9999-9999-9999-999999999999'
        client.context.device = None
        client.track_request('test', 'http://tempuri.org', True, 'START_TIME',
                             13, 42, 'OPTIONS', {'foo': 'bar'}, {'x': 42},
                             'ID_PLACEHOLDER')
        client.flush()
        expected = '{"ver": 1, "name": "Microsoft.ApplicationInsights.Request", "time": "TIME_PLACEHOLDER", "sampleRate": 100.0, "iKey": "99999999-9999-9999-9999-999999999999", "tags": {"ai.device.id": "DEVICE_ID_PLACEHOLDER", "ai.device.locale": "DEVICE_LOCALE_PLACEHOLDER", "ai.device.osVersion": "DEVICE_OS_VERSION_PLACEHOLDER", "ai.device.type": "DEVICE_TYPE_PLACEHOLDER", "ai.internal.sdkVersion": "SDK_VERSION_PLACEHOLDER", "ai.session.id": "BOTSESSION", "ai.user.id": "BOTUSER"}, "data": {"baseType": "RequestData", "baseData": {"ver": 2, "id": "ID_PLACEHOLDER", "name": "test", "duration": "00:00:00.013", "responseCode": "42", "success": true, "url": "http://tempuri.org", "properties": {"NEW_PROP": "MYPROP", "foo": "bar"}, "measurements": {"x": 42}}}}'
        sender.data.time = 'TIME_PLACEHOLDER'
        sender.data.tags['ai.internal.sdkVersion'] = 'SDK_VERSION_PLACEHOLDER'
        sender.data.tags['ai.device.id'] = "DEVICE_ID_PLACEHOLDER"
        sender.data.tags['ai.device.locale'] = "DEVICE_LOCALE_PLACEHOLDER"
        sender.data.tags[
            'ai.device.osVersion'] = "DEVICE_OS_VERSION_PLACEHOLDER"
        sender.data.tags['ai.device.type'] = "DEVICE_TYPE_PLACEHOLDER"
        actual = json.dumps(sender.data.write())
        self.maxDiff = None
        self.assertEqual(expected, actual)
 def test_track_request_works_as_expected(self):
     sender = MockTelemetrySender()
     queue = channel.SynchronousQueue(sender)
     client = TelemetryClient(channel.TelemetryChannel(context=None, queue=queue))
     client.context.instrumentation_key = '99999999-9999-9999-9999-999999999999'
     client.context.device = None
     client.track_request('test', 'http://tempuri.org', True, 'START_TIME', 13, '42', 'OPTIONS', { 'foo': 'bar' }, { 'x': 42 })
     client.flush()
     expected = '{"ver": 1, "name": "Microsoft.ApplicationInsights.Request", "time": "TIME_PLACEHOLDER", "sampleRate": 100.0, "iKey": "99999999-9999-9999-9999-999999999999", "tags": {"ai.internal.sdkVersion": "SDK_VERSION_PLACEHOLDER"}, "data": {"baseType": "RequestData", "baseData": {"ver": 2, "id": "ID_PLACEHOLDER", "name": "test", "startTime": "START_TIME", "duration": "00:00:00.013", "responseCode": "42", "success": true, "httpMethod": "OPTIONS", "url": "http://tempuri.org", "properties": {"foo": "bar"}, "measurements": {"x": 42}}}}'
     sender.data.time = 'TIME_PLACEHOLDER'
     sender.data.tags['ai.internal.sdkVersion'] = 'SDK_VERSION_PLACEHOLDER'
     sender.data.data.base_data.id = 'ID_PLACEHOLDER'
     actual = json.dumps(sender.data.write())
     self.maxDiff = None
     self.assertEqual(expected, actual)
 def test_track_request_works_as_expected(self):
     sender = MockTelemetrySender()
     queue = channel.SynchronousQueue(sender)
     client = TelemetryClient(channel.TelemetryChannel(context=None, queue=queue))
     client.context.instrumentation_key = '99999999-9999-9999-9999-999999999999'
     client.context.device = None
     client.track_request('test', 'http://tempuri.org', True, 'START_TIME', 13, '42', 'OPTIONS', { 'foo': 'bar' }, { 'x': 42 })
     client.flush()
     expected = '{"ver": 1, "name": "Microsoft.ApplicationInsights.Request", "time": "TIME_PLACEHOLDER", "sampleRate": 100.0, "iKey": "99999999-9999-9999-9999-999999999999", "tags": {"ai.internal.sdkVersion": "SDK_VERSION_PLACEHOLDER"}, "data": {"baseType": "RequestData", "baseData": {"ver": 2, "id": "ID_PLACEHOLDER", "name": "test", "startTime": "START_TIME", "duration": "00:00:00.013", "responseCode": "42", "success": true, "httpMethod": "OPTIONS", "url": "http://tempuri.org", "properties": {"foo": "bar"}, "measurements": {"x": 42}}}}'
     sender.data.time = 'TIME_PLACEHOLDER'
     sender.data.tags['ai.internal.sdkVersion'] = 'SDK_VERSION_PLACEHOLDER'
     sender.data.data.base_data.id = 'ID_PLACEHOLDER'
     actual = json.dumps(sender.data.write())
     self.maxDiff = None
     self.assertEqual(expected, actual)
예제 #5
0
class AppInsights(object):
    """ This class represents a plugin that enables request telemetry,
     logging for a Bottle application. The telemetry
    will be sent to Application Insights service using the supplied
    instrumentation key.

    The following Bottle config variables can be used to configure the extension:

    - Set ``APPINSIGHTS_INSTRUMENTATIONKEY`` to a string to provide the
      instrumentation key to send telemetry to application insights.
      Alternatively, this value can also be provided via an environment variable
      of the same name.

    - Set ``APPINSIGHTS_ENDPOINT_URI`` to a string to customize the telemetry
      endpoint to which Application Insights will send the telemetry.

    - Set ``APPINSIGHTS_DISABLE_REQUEST_LOGGING`` to ``False`` to disable
      logging of Bottle requests to Application Insights.

    .. code:: python

            from bottle import run, Bottle
            from applicationinsights.bottle.plugin import AppInsights

            app = Bottle()
            app.config["APPINSIGHTS_INSTRUMENTATIONKEY"] = "<YOUR INSTRUMENTATION KEY GOES HERE>"
            app.install(AppInsights())

            @app.route('/hello')
            def hello():
                return "Hello World!"

            if __name__ == '__main__':
                run(app, host='localhost', port=8080)
    """

    name = "appinsights"
    api = 2

    def __init__(self):
        """
        Initialize a new instance of the extension.

        """
        self._key = None
        self._endpoint_uri = None
        self._channel = None
        self._tc = None

    def setup(self, app):
        """
        Initializes the plugin for the provided Bottle application.

        Args:
            app (bottle.Bottle). the Bottle application for which to initialize the extension.
        """
        self._key = app.config.get(CONF_KEY) or getenv(CONF_KEY)

        if not self._key:
            return

        self._endpoint_uri = app.config.get(CONF_ENDPOINT_URI)
        sender = AsynchronousSender(self._endpoint_uri)

        queue = AsynchronousQueue(sender)
        self._channel = TelemetryChannel(None, queue)
        self._tc = TelemetryClient(self._key, self._channel)

        self.context.cloud.role_instance = platform.node()

    def close(self):
        self.flush()

    @property
    def context(self):
        """
        Accesses the telemetry context.

        Returns:
            (applicationinsights.channel.TelemetryContext). The Application Insights telemetry context.
        """
        return self._channel.context

    def apply(self, callback, route):
        """
        Sets up request logging unless ``APPINSIGHTS_DISABLE_REQUEST_LOGGING``
        is set in the Bottle config.
        """
        if route.app.config.get(CONF_DISABLE_REQUEST_LOGGING,
                                False) or self._tc is None:
            return callback

        def wrapper(*args, **kwargs):
            start_time = current_milli_time()
            result = callback(*args, **kwargs)
            try:
                duration = current_milli_time() - start_time

                self._tc.track_request(route.method + " " + route.rule,
                                       route.rule,
                                       bottle.response.status_code < 400,
                                       start_time, duration,
                                       bottle.response.status_code,
                                       route.method)
            finally:
                return result

        return wrapper

    def flush(self):
        """Flushes the queued up telemetry to the service.
        """
        if self._tc:
            self._tc.flush()
예제 #6
0
class ApplicationInsightsTelemetryClient(BotTelemetryClient):
    def __init__(self, instrumentation_key: str):
        self._instrumentation_key = instrumentation_key
        self._client = TelemetryClient(self._instrumentation_key)

        # Telemetry Processor
        def telemetry_processor(data, context):
            post_data = IntegrationPostData().activity_json
            # Override session and user id
            from_prop = post_data['from'] if 'from' in post_data else None
            user_id = from_prop['id'] if from_prop != None else None
            channel_id = post_data[
                'channelId'] if 'channelId' in post_data else None
            conversation = post_data[
                'conversation'] if 'conversation' in post_data else None
            conversation_id = conversation[
                'id'] if 'id' in conversation else None
            context.user.id = channel_id + user_id
            context.session.id = conversation_id

            # Additional bot-specific properties
            if 'activityId' in post_data:
                data.properties["activityId"] = post_data['activityId']
            if 'channelId' in post_data:
                data.properties["channelId"] = post_data['channelId']
            if 'activityType' in post_data:
                data.properties["activityType"] = post_data['activityType']

        self._client.add_telemetry_processor(telemetry_processor)

    def track_pageview(self,
                       name: str,
                       url: str,
                       duration: int = 0,
                       properties: Dict[str, object] = None,
                       measurements: Dict[str, object] = None) -> None:
        """
        Send information about the page viewed in the application (a web page for instance).
        :param name: the name of the page that was viewed.
        :param url: the URL of the page that was viewed.
        :param duration: the duration of the page view in milliseconds. (defaults to: 0)
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        """
        self._client.track_pageview(name, url, duration, properties,
                                    measurements)

    def track_exception(self,
                        type_exception: type = None,
                        value: Exception = None,
                        tb: traceback = None,
                        properties: Dict[str, object] = None,
                        measurements: Dict[str, object] = None) -> None:
        """ 
        Send information about a single exception that occurred in the application.
        :param type_exception: the type of the exception that was thrown.
        :param value: the exception that the client wants to send.
        :param tb: the traceback information as returned by :func:`sys.exc_info`.
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        """
        self._client.track_exception(type_exception, value, tb, properties,
                                     measurements)

    def track_event(self,
                    name: str,
                    properties: Dict[str, object] = None,
                    measurements: Dict[str, object] = None) -> None:
        """ 
        Send information about a single event that has occurred in the context of the application.
        :param name: the data to associate to this event.
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        """
        self._client.track_event(name, properties, measurements)

    def track_metric(self,
                     name: str,
                     value: float,
                     type: TelemetryDataPointType = None,
                     count: int = None,
                     min: float = None,
                     max: float = None,
                     std_dev: float = None,
                     properties: Dict[str, object] = None) -> NotImplemented:
        """
        Send information about a single metric data point that was captured for the application.
        :param name: The name of the metric that was captured.
        :param value: The value of the metric that was captured.
        :param type: The type of the metric. (defaults to: TelemetryDataPointType.aggregation`)
        :param count: the number of metrics that were aggregated into this data point. (defaults to: None)
        :param min: the minimum of all metrics collected that were aggregated into this data point. (defaults to: None)
        :param max: the maximum of all metrics collected that were aggregated into this data point. (defaults to: None)
        :param std_dev: the standard deviation of all metrics collected that were aggregated into this data point. (defaults to: None)
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        """
        self._client.track_metric(name, value, type, count, min, max, std_dev,
                                  properties)

    def track_trace(self,
                    name: str,
                    properties: Dict[str, object] = None,
                    severity=None):
        """
        Sends a single trace statement.
        :param name: the trace statement.\n
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)\n
        :param severity: the severity level of this trace, one of DEBUG, INFO, WARNING, ERROR, CRITICAL
        """
        self._client.track_trace(name, properties, severity)

    def track_request(self,
                      name: str,
                      url: str,
                      success: bool,
                      start_time: str = None,
                      duration: int = None,
                      response_code: str = None,
                      http_method: str = None,
                      properties: Dict[str, object] = None,
                      measurements: Dict[str, object] = None,
                      request_id: str = None):
        """
        Sends a single request that was captured for the application.
        :param name: The name for this request. All requests with the same name will be grouped together.
        :param url: The actual URL for this request (to show in individual request instances).
        :param success: True if the request ended in success, False otherwise.
        :param start_time: the start time of the request. The value should look the same as the one returned by :func:`datetime.isoformat()` (defaults to: None)
        :param duration: the number of milliseconds that this request lasted. (defaults to: None)
        :param response_code: the response code that this request returned. (defaults to: None)
        :param http_method: the HTTP method that triggered this request. (defaults to: None)
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        :param request_id: the id for this request. If None, a new uuid will be generated. (defaults to: None)
        """
        self._client.track_request(name, url, success, start_time, duration,
                                   response_code, http_method, properties,
                                   measurements, request_id)

    def track_dependency(self,
                         name: str,
                         data: str,
                         type: str = None,
                         target: str = None,
                         duration: int = None,
                         success: bool = None,
                         result_code: str = None,
                         properties: Dict[str, object] = None,
                         measurements: Dict[str, object] = None,
                         dependency_id: str = None):
        """
        Sends a single dependency telemetry that was captured for the application.
        :param name: the name of the command initiated with this dependency call. Low cardinality value. Examples are stored procedure name and URL path template.
        :param data: the command initiated by this dependency call. Examples are SQL statement and HTTP URL with all query parameters.
        :param type: the dependency type name. Low cardinality value for logical grouping of dependencies and interpretation of other fields like commandName and resultCode. Examples are SQL, Azure table, and HTTP. (default to: None)
        :param target: the target site of a dependency call. Examples are server name, host address. (default to: None)
        :param duration: the number of milliseconds that this dependency call lasted. (defaults to: None)
        :param success: true if the dependency call ended in success, false otherwise. (defaults to: None)
        :param result_code: the result code of a dependency call. Examples are SQL error code and HTTP status code. (defaults to: None)
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        :param id: the id for this dependency call. If None, a new uuid will be generated. (defaults to: None)
        """
        self._client.track_dependency(name, data, type, target, duration,
                                      success, result_code, properties,
                                      measurements, dependency_id)

    def flush(self):
        """Flushes data in the queue. Data in the queue will be sent either immediately irrespective of what sender is
        being used.
        """
        self._client.flush()
class AppinsightsBotTelemetryClient(BotTelemetryClient):
    def __init__(self, instrumentation_key: str):
        self._instrumentation_key = instrumentation_key

        self._context = TelemetryContext()
        context.instrumentation_key = self._instrumentation_key
        # context.user.id = 'BOTID'        # telemetry_channel.context.session.
        # context.session.id = 'BOTSESSION'

        # set up channel with context
        self._channel = TelemetryChannel(context)
        # telemetry_channel.context.properties['my_property'] = 'my_value'

        self._client = TelemetryClient(self._instrumentation_key,
                                       self._channel)

    def track_pageview(self,
                       name: str,
                       url: str,
                       duration: int = 0,
                       properties: Dict[str, object] = None,
                       measurements: Dict[str, object] = None) -> None:
        """
        Send information about the page viewed in the application (a web page for instance).
        :param name: the name of the page that was viewed.
        :param url: the URL of the page that was viewed.
        :param duration: the duration of the page view in milliseconds. (defaults to: 0)
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        """
        self._client.track_pageview(name, url, duration, properties,
                                    measurements)

    def track_exception(self,
                        type_exception: type = None,
                        value: Exception = None,
                        tb: traceback = None,
                        properties: Dict[str, object] = None,
                        measurements: Dict[str, object] = None) -> None:
        """ 
        Send information about a single exception that occurred in the application.
        :param type_exception: the type of the exception that was thrown.
        :param value: the exception that the client wants to send.
        :param tb: the traceback information as returned by :func:`sys.exc_info`.
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        """
        self._client.track_exception(type_exception, value, tb, properties,
                                     measurements)

    def track_event(self,
                    name: str,
                    properties: Dict[str, object] = None,
                    measurements: Dict[str, object] = None) -> None:
        """ 
        Send information about a single event that has occurred in the context of the application.
        :param name: the data to associate to this event.
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        """
        self._client.track_event(name, properties, measurements)

    def track_metric(self,
                     name: str,
                     value: float,
                     type: TelemetryDataPointType = None,
                     count: int = None,
                     min: float = None,
                     max: float = None,
                     std_dev: float = None,
                     properties: Dict[str, object] = None) -> NotImplemented:
        """
        Send information about a single metric data point that was captured for the application.
        :param name: The name of the metric that was captured.
        :param value: The value of the metric that was captured.
        :param type: The type of the metric. (defaults to: TelemetryDataPointType.aggregation`)
        :param count: the number of metrics that were aggregated into this data point. (defaults to: None)
        :param min: the minimum of all metrics collected that were aggregated into this data point. (defaults to: None)
        :param max: the maximum of all metrics collected that were aggregated into this data point. (defaults to: None)
        :param std_dev: the standard deviation of all metrics collected that were aggregated into this data point. (defaults to: None)
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        """
        self._client.track_metric(name, value, type, count, min, max, std_dev,
                                  properties)

    def track_trace(self,
                    name: str,
                    properties: Dict[str, object] = None,
                    severity=None):
        """
        Sends a single trace statement.
        :param name: the trace statement.\n
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)\n
        :param severity: the severity level of this trace, one of DEBUG, INFO, WARNING, ERROR, CRITICAL
        """
        self._client.track_trace(name, properties, severity)

    def track_request(self,
                      name: str,
                      url: str,
                      success: bool,
                      start_time: str = None,
                      duration: int = None,
                      response_code: str = None,
                      http_method: str = None,
                      properties: Dict[str, object] = None,
                      measurements: Dict[str, object] = None,
                      request_id: str = None):
        """
        Sends a single request that was captured for the application.
        :param name: The name for this request. All requests with the same name will be grouped together.
        :param url: The actual URL for this request (to show in individual request instances).
        :param success: True if the request ended in success, False otherwise.
        :param start_time: the start time of the request. The value should look the same as the one returned by :func:`datetime.isoformat()` (defaults to: None)
        :param duration: the number of milliseconds that this request lasted. (defaults to: None)
        :param response_code: the response code that this request returned. (defaults to: None)
        :param http_method: the HTTP method that triggered this request. (defaults to: None)
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        :param request_id: the id for this request. If None, a new uuid will be generated. (defaults to: None)
        """
        self._client.track_request(name, url, success, start_time, duration,
                                   response_code, http_method, properties,
                                   measurements, request_id)

    def track_dependency(self,
                         name: str,
                         data: str,
                         type: str = None,
                         target: str = None,
                         duration: int = None,
                         success: bool = None,
                         result_code: str = None,
                         properties: Dict[str, object] = None,
                         measurements: Dict[str, object] = None,
                         dependency_id: str = None):
        """
        Sends a single dependency telemetry that was captured for the application.
        :param name: the name of the command initiated with this dependency call. Low cardinality value. Examples are stored procedure name and URL path template.
        :param data: the command initiated by this dependency call. Examples are SQL statement and HTTP URL with all query parameters.
        :param type: the dependency type name. Low cardinality value for logical grouping of dependencies and interpretation of other fields like commandName and resultCode. Examples are SQL, Azure table, and HTTP. (default to: None)
        :param target: the target site of a dependency call. Examples are server name, host address. (default to: None)
        :param duration: the number of milliseconds that this dependency call lasted. (defaults to: None)
        :param success: true if the dependency call ended in success, false otherwise. (defaults to: None)
        :param result_code: the result code of a dependency call. Examples are SQL error code and HTTP status code. (defaults to: None)
        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to: None)
        :param id: the id for this dependency call. If None, a new uuid will be generated. (defaults to: None)
        """
        self._client.track_dependency(name, data, type, target, duration,
                                      success, result_code, properties,
                                      measurements, dependency_id)
예제 #8
0
def send_requests(client: TelemetryClient, num_requests: int):
    for _ in range(num_requests):
        request = generate_request()
        client.track_request(**request)
        LOG.info('sent request %r', request)