예제 #1
0
class HTTPRequestHandler(BaseHTTPRequestHandler):
    """Custom HTTPRequestHandler class."""
    @wtt.spanned(
        span_namer=wtt.SpanNamer(use_this_arg="self.command"),
        kind=wtt.SpanKind.SERVER,
        carrier="self.headers",
    )
    def do_GET(self) -> None:  # pylint: disable=invalid-name
        """Handle GET command."""
        logging.info("Example HTTP Server Handler with 'kind=SpanKind.SERVER'")

        all_responses = {
            "ham": "ham sandwich!",
            "cheese": "cheese sandwich!",
            "bacon": "BLT!",
        }

        if self.path in all_responses:
            self.send_response(200)

            # send header first
            self.send_header("Content-type", "text-html")
            self.end_headers()

            # send file content to client
            self.wfile.write(all_responses[self.path].encode())

        else:
            self.send_error(404, "Ingredient not found!")

            logging.critical("Server shutting down (to end testing)")
            sys.exit(0)
예제 #2
0
class ExternalClass:
    """Handle methods using a Span instance as a instance variable."""

    def __init__(self, span: wtt.Span) -> None:
        self.span = span
        self.name = "TheExternalClass!"

    @wtt.spanned(
        span_namer=wtt.SpanNamer(
            literal_name="DISJOINT_SPANNED_METHOD",
            use_this_arg="self.name",
            use_function_name=False,
        )
    )  # sets current_span
    def disjoint_spanned_method(self) -> None:
        """Do some things with a new disjoint span."""
        print("disjoint_spanned_method")
        assert wtt.get_current_span().is_recording()
        assert self.span.is_recording()
        assert self.span != wtt.get_current_span()

        @wtt.evented(all_args=True)
        def inner_event_1(name: str, height: int) -> None:
            print(name)
            print(height)

        @wtt.evented(span="span", all_args=True)
        def inner_event_2(name: str, height: int, span: wtt.Span) -> None:
            assert span.is_recording()
            print(name)
            print(height)

        inner_event_1("Bar", 185)
        inner_event_2("Foo", 177, self.span)
예제 #3
0
import coloredlogs  # type: ignore[import]

if "examples" not in os.listdir():
    raise RuntimeError("Script needs to be ran from root of repository.")

sys.path.append(".")
import wipac_telemetry.tracing_tools as wtt  # noqa: E402 # pylint: disable=C0413,E0401

ADDRESS = "127.0.0.1"
PORT = 2000

########################################################################################


@wtt.spanned(span_namer=wtt.SpanNamer(literal_name="TheBestClient"),
             kind=wtt.SpanKind.CLIENT)
def client() -> None:
    """Run HTTP client."""
    logging.info("Example HTTP Client with 'kind=SpanKind.CLIENT'")

    logging.debug("http client is starting...")
    time.sleep(0.5)

    conn = http.client.HTTPConnection(ADDRESS, PORT)  # create a connection
    logging.debug("http client is running...")
    time.sleep(0.5)

    for msg in ["ham", "cheese", "bacon", "nails"]:
        wtt.add_event(
            "Outgoing Server Request",
예제 #4
0
class RestHandler(tornado.web.RequestHandler):
    """Default REST handler."""
    def __init__(self, *args, **kwargs) -> None:
        self.server_header = ''
        try:
            super().__init__(*args, **kwargs)
        except Exception:
            logging.error('error', exc_info=True)

    def initialize(self,
                   debug=False,
                   auth=None,
                   auth_url=None,
                   module_auth_key='',
                   server_header='',
                   route_stats=None,
                   **kwargs):
        super().initialize(**kwargs)
        self.debug = debug
        self.auth = auth
        self.auth_url = auth_url
        self.auth_data = {}
        self.auth_key = None
        self.module_auth_key = module_auth_key
        self.server_header = server_header
        self.route_stats = route_stats

    @wtt.spanned(
        span_namer=wtt.SpanNamer(use_this_arg='self.request.method'),
        kind=wtt.SpanKind.SERVER,
        these=["self.request.method", "self.request.path"],
        carrier="self.request.headers",
    )
    async def _execute(self, *args: Any, **kwargs: Any) -> None:
        """Call implemented methods.

        NOTE: This is the closest common call-stack ancestor of
            - `prepare()`,
            - "method handlers" (`get()`, `post()`, ...),
            - `on_finish()`,
            - etc.
        """
        return await super()._execute(*args, **kwargs)

    def set_default_headers(self):
        self._headers['Server'] = self.server_header

    def get_template_namespace(self):
        namespace = super().get_template_namespace()
        namespace['version'] = rest_tools.__version__
        return namespace

    def get_current_user(self):
        """Get the current user, and set auth-related attributes."""
        try:
            type, token = self.request.headers['Authorization'].split(' ', 1)
            if type.lower() != 'bearer':
                raise Exception('bad header type')
            logger.debug('token: %r', token)
            data = self.auth.validate(token)
            self.auth_data = data
            self.auth_key = token
            if "role" in self.auth_data:
                wtt.get_current_span().set_attribute('self.auth_data.role',
                                                     self.auth_data['role'])
            return data['sub']
        # Auth Failed
        except Exception:
            if self.debug and 'Authorization' in self.request.headers:
                logger.info('Authorization: %r',
                            self.request.headers['Authorization'])
            logger.info('failed auth', exc_info=True)

        return None

    @wtt.evented()
    def prepare(self):
        """Prepare before http-method request handlers."""
        if self.route_stats is not None:
            stat = self.route_stats[self.request.path]
            if stat.is_overloaded():
                backoff = stat.get_backoff_time()
                logger.warn('Server is overloaded, backoff %r', backoff)
                self.set_header('Retry-After', backoff)
                raise tornado.web.HTTPError(503, reason="server overloaded")
            self.start_time = time.time()

    @wtt.evented()
    def on_finish(self):
        """Cleanup after http-method request handlers."""
        if self.route_stats is not None and self.get_status() < 500:
            stat = self.route_stats[self.request.path]
            stat.append(time.time() - self.start_time)

    @wtt.evented(all_args=True)
    def write_error(self, status_code=500, **kwargs):
        """Write out custom error json."""
        data = {
            'code': status_code,
            'error': self._reason,
        }
        self.write(data)
        self.finish()

    def get_json_body_argument(
        self,
        name: str,
        default: Any = arghandler.NO_DEFAULT,
        type: Optional[type] = None,
        choices: Optional[List[Any]] = None,
        forbiddens: Optional[List[Any]] = None,
    ) -> Any:
        """Get argument from the JSON-decoded request-body.

        If no `default` is provided, and the argument is not present, raise `400`.

        Arguments:
            name -- the argument's name

        Keyword Arguments:
            default -- a default value to use if the argument is not present
            type -- optionally, type-check the argument's value (raise `400` for invalid value)
            choices -- a list of valid argument values (raise `400`, if arg's value is not in list)
            forbiddens -- a list of disallowed argument values (raise `400`, if arg's value is in list)

        Returns:
            Any -- the argument's value, unaltered
        """
        return arghandler.ArgumentHandler.get_json_body_argument(
            self.request.body, name, default, type, choices, forbiddens)

    def get_argument(
        self,
        name: str,
        default: Any = arghandler.NO_DEFAULT,
        strip: bool = True,
        type: Optional[type] = None,
        choices: Optional[List[Any]] = None,
        forbiddens: Optional[List[Any]] = None,
    ) -> Any:
        """Get argument from query base-arguments / JSON-decoded request-body.

        If no `default` is provided, and the argument is not present, raise `400`.

        Arguments:
            name -- the argument's name

        Keyword Arguments:
            default -- a default value to use if the argument is not present
            strip {`bool`} -- whether to `str.strip()` the arg's value (default: {`True`})
            type -- optionally, type-cast/check the argument's value (raise `400` for invalid value)
            choices -- a list of valid argument values (raise `400`, if arg's value is not in list)
            forbiddens -- a list of disallowed argument values (raise `400`, if arg's value is in list)

        Returns:
            Any -- the argument's value, possibly stripped/type-casted
        """
        return arghandler.ArgumentHandler.get_argument(self.request.body,
                                                       super().get_argument,
                                                       name, default, strip,
                                                       type, choices,
                                                       forbiddens)
예제 #5
0
class RestClient:
    """A REST client with token handling.

    Args:
        address (str): base address of REST API
        token (str): (optional) access token, or a function generating an access token
        timeout (int): (optional) request timeout (default: 60s)
        retries (int): (optional) number of retries to attempt (default: 10)
        username (str): (optional) auth-basic username
        password (str): (optional) auth-basic password
    """
    def __init__(self,
                 address: str,
                 token: Optional[Union[str, bytes,
                                       Callable[[], Union[str,
                                                          bytes]]]] = None,
                 timeout: float = 60.0,
                 retries: int = 10,
                 **kwargs: Any) -> None:
        self.address = address
        self.timeout = timeout
        self.retries = retries
        self.kwargs = kwargs
        self.logger = logging.getLogger('RestClient')

        # token handling
        self._token_expire_delay_offset = 5
        self.access_token: Optional[Union[str, bytes]] = None
        self.token_func: Optional[Callable[[], Union[str, bytes]]] = None
        if token:
            if isinstance(token, (str, bytes)):
                self.access_token = token
            elif callable(token):
                self.token_func = token

        self.session = self.open()  # start session

    def open(self, sync: bool = False) -> requests.Session:
        """Open the http session."""
        self.logger.debug('establish REST http session')
        if sync:
            self.session = Session(self.retries)
        else:
            self.session = AsyncSession(self.retries)
        self.session.headers = {  # type: ignore[assignment]
            'Content-Type': 'application/json',
        }
        if 'username' in self.kwargs and 'password' in self.kwargs:
            self.session.auth = (self.kwargs['username'],
                                 self.kwargs['password'])
        if 'sslcert' in self.kwargs:
            if 'sslkey' in self.kwargs:
                self.session.cert = (self.kwargs['sslcert'],
                                     self.kwargs['sslkey'])
            else:
                self.session.cert = self.kwargs['sslcert']
        if 'cacert' in self.kwargs:
            self.session.verify = self.kwargs['cacert']

        return self.session

    def close(self) -> None:
        """Close the http session."""
        self.logger.info('close REST http session')
        if self.session:
            self.session.close()

    def _get_token(self) -> None:
        if self.access_token:
            # check if expired
            try:
                # NOTE: PyJWT mis-type-hinted arg #1 as a str, but byte is also fine
                # https://github.com/jpadilla/pyjwt/pull/605#issuecomment-772082918
                data = jwt.decode(
                    self.access_token,  # type: ignore[arg-type]
                    algorithms=['RS256', 'RS512'],
                    options={"verify_signature": False})
                # account for an X second delay over the wire, so expire sooner
                if data['exp'] < time.time() + self._token_expire_delay_offset:
                    raise Exception()
                return
            except Exception:
                self.access_token = None
                self.logger.debug('token expired')

        try:
            self.access_token = self.token_func()  # type: ignore[misc]
        except Exception:
            self.logger.warning('acquiring access token failed')
            raise

    def _prepare(
        self,
        method: str,
        path: str,
        args: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
    ) -> Tuple[str, Dict[str, Any]]:
        """Internal method for preparing requests."""
        if not args:
            args = {}

        # auto-inject the current span's info into the HTTP headers
        if wtt.get_current_span().is_recording():
            wtt.propagations.inject_span_carrier(
                self.session.headers)  # type: ignore[arg-type]

        if path.startswith('/'):
            path = path[1:]
        url = os.path.join(self.address, path)

        kwargs: Dict[str, Any] = {'timeout': self.timeout}

        if method in ('GET', 'HEAD'):
            # args should be urlencoded
            kwargs['params'] = args
        else:
            kwargs['json'] = args

        if self.token_func:
            self._get_token()

        if not headers:
            headers = {}

        if self.access_token:
            headers['Authorization'] = 'Bearer ' + _to_str(self.access_token)

        if headers:
            kwargs['headers'] = headers

        return (url, kwargs)

    def _decode(self, content: Union[str, bytes, bytearray]) -> JSONType:
        """Internal method for translating response from json."""
        if not content:
            self.logger.info('request returned empty string')
            return None
        try:
            return json_decode(content)
        except Exception:
            self.logger.info('json data: %r', content)
            raise

    @wtt.spanned(span_namer=wtt.SpanNamer(use_this_arg='method'),
                 these=['method', 'path', 'self.address'],
                 kind=wtt.SpanKind.CLIENT)
    async def request(
        self,
        method: str,
        path: str,
        args: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
    ) -> JSONType:
        """Send request to REST Server.

        Async request - use with coroutines.

        Args:
            method (str): the http method
            path (str): the url path on the server
            args (dict): any arguments to pass
            headers (dict): any headers to pass to the request

        Returns:
            dict: json dict or raw string
        """
        url, kwargs = self._prepare(method, path, args, headers)
        try:
            # session: AsyncSession; So, self.session.request() -> Future
            r: requests.Response = await asyncio.wrap_future(
                self.session.request(method, url,
                                     **kwargs))  # type: ignore[arg-type]
            r.raise_for_status()
            return self._decode(r.content)
        except requests.exceptions.HTTPError as e:
            if method == 'DELETE' and e.response.status_code == 404:
                raise  # skip the logging for an expected error
            self.logger.info('bad request: %s %s %r',
                             method,
                             path,
                             args,
                             exc_info=True)
            raise

    @wtt.spanned(span_namer=wtt.SpanNamer(use_this_arg='method'),
                 these=['method', 'path', 'self.address'],
                 kind=wtt.SpanKind.CLIENT)
    def request_seq(
        self,
        method: str,
        path: str,
        args: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
    ) -> JSONType:
        """Send request to REST Server.

        Sequential version of `request`.

        Args:
            method (str): the http method
            path (str): the url path on the server
            args (dict): any arguments to pass
            headers (dict): any headers to pass to the request

        Returns:
            dict: json dict or raw string
        """
        s = self.session
        try:
            self.open(sync=True)
            url, kwargs = self._prepare(method, path, args, headers)
            r = self.session.request(method, url, **kwargs)
            r.raise_for_status()
            return self._decode(r.content)
        finally:
            self.session = s

    @wtt.spanned(span_namer=wtt.SpanNamer(use_this_arg='method'),
                 these=['method', 'path', 'self.address'],
                 kind=wtt.SpanKind.CLIENT)
    def request_stream(
        self,
        method: str,
        path: str,
        args: Optional[Dict[str, Any]] = None,
        headers: Optional[Dict[str, str]] = None,
        chunk_size: Optional[int] = 8096,
    ) -> Generator[JSONType, None, None]:
        """Send request to REST Server, and stream results back.

        `chunk_size=None` will read data as it arrives
        in whatever size the chunks are received. `chunk_size`<`1`
        will be treated as `chunk_size=None`

        Args:
            method (str): the http method
            path (str): the url path on the server
            args (dict): any arguments to pass
            headers (dict): any headers to pass to the request
            chunk_size (int): chunk size (see above)

        Returns:
            dict: json dict or raw string
        """
        if chunk_size is not None and chunk_size < 1:
            chunk_size = None

        s = self.session
        try:
            self.open(sync=True)
            url, kwargs = self._prepare(method, path, args, headers)
            resp = self.session.request(method, url, stream=True, **kwargs)
            resp.raise_for_status()
            for line in resp.iter_lines(chunk_size=chunk_size,
                                        delimiter=b'\n'):
                decoded = self._decode(line.strip())
                if decoded:  # skip `None`
                    yield decoded
        finally:
            self.session = s
예제 #6
0
class DemoClass:
    """Handle methods using a Span instance as a instance variable."""

    def __init__(self) -> None:
        self.span: Optional[wtt.Span] = None
        self.name = "DEMO!"

    @wtt.spanned(
        span_namer=wtt.SpanNamer(
            literal_name="TheBestPrepare", use_this_arg="self.name"
        ),
        behavior=wtt.SpanBehavior.ONLY_END_ON_EXCEPTION,
    )
    def prepare(self) -> None:
        """Do some things and start an independent span."""
        self.span = wtt.get_current_span()
        assert self.span.is_recording()
        self.span.add_event("(method) started span from instance method")
        time.sleep(3)

        @wtt.respanned(None, wtt.SpanBehavior.END_ON_EXIT, all_args=True)
        def illegal(num: int) -> None:
            # this would end the span before the caller does
            pass

        @wtt.respanned(None, wtt.SpanBehavior.ONLY_END_ON_EXCEPTION, all_args=True)
        def legal_and_rare(num: int) -> None:
            # this could end the span, which may or may not be wanted
            pass

        @wtt.respanned(None, wtt.SpanBehavior.DONT_END, all_args=True)
        def legal_and_fine(num: int) -> None:
            # this is okay, and a quick way to add to the span
            pass

        legal_and_rare(11)
        legal_and_fine(22)
        try:
            illegal(33)
        except wtt.spans.InvalidSpanBehavior:
            assert 1
        else:
            assert 0

    @wtt.respanned(
        "self.span", wtt.SpanBehavior.ONLY_END_ON_EXCEPTION, attributes={"a": 2}
    )
    def process(self) -> None:
        """Do some things and reuse a span."""
        time.sleep(2)
        assert self.span == wtt.get_current_span()
        assert wtt.get_current_span().is_recording()

        # this won't end the span
        try:
            raise KeyError()
        except:  # noqa: E722 # pylint: disable=bare-except
            pass

    @wtt.respanned("self.span", wtt.SpanBehavior.DONT_END)
    def process_with_exception(self) -> None:
        """Do some things and reuse a span."""
        time.sleep(1)
        assert self.span == wtt.get_current_span()
        assert wtt.get_current_span().is_recording()

        # this won't end the span so traces won't be sent,
        # unless the exception is excepted by the caller
        raise Exception("An exception!")

    @wtt.respanned(
        "self.span", wtt.SpanBehavior.END_ON_EXIT, attributes={"b": 3}
    )  # auto-ends
    def finish(self) -> None:
        """Do some things, reuse a span, then close that span."""
        time.sleep(1)
        assert self.span == wtt.get_current_span()
        assert wtt.get_current_span().is_recording()

    ##################

    # NOT OKAY - just avoid __del__ + Span.end(), interaction seems undefined
    # def __del__(self) -> None:
    #     """ERROR: ending an instance-dependent span in __del__ is not supported, will break."""
    #     if self.span:
    #         self.span.add_event("__del__")
    #         self.span.end()

    # USE THIS INSTEAD
    def end(self) -> None:
        """Clean up."""
        if self.span:
            self.span.add_event("manually ending span")
            self.span.end()
예제 #7
0
            print(name)
            print(height)

        inner_event_1("Bar", 185)
        inner_event_2("Foo", 177, self.span)

    # NOT OKAY - just avoid __del__ + Span.end(), interaction seems undefined
    # def __del__(self) -> None:
    #     """Clean up."""
    #     if self.span:
    #         self.span.add_event("__del__")
    #         self.span.end()


@wtt.spanned(
    span_namer=wtt.SpanNamer(use_function_name=False),
    attributes={"a": 1},
)  # auto end-on-exit
def injected_span_pass_to_instance() -> ExternalClass:
    """Inject a span then pass onto an instance."""
    assert wtt.get_current_span().is_recording()

    wtt.get_current_span().set_attribute("Random Int", random.randint(0, 9))
    instance = ExternalClass(wtt.get_current_span())
    instance.disjoint_spanned_method()
    return instance


########################################################################################

예제 #8
0
    msg = "Hello World!"
    print(msg)
    logging.info(msg)


class Example2:
    """An example with an class instance method."""
    @wtt.spanned()
    def example_2_instance_method(self) -> None:
        """Print and log simple message."""
        msg = "Hello World!"
        print(msg)
        logging.info(msg)


@wtt.spanned(wtt.SpanNamer("my-span"))
def example_3_with_name() -> None:
    """Print and log simple message."""
    msg = "Hello World!"
    print(msg)
    logging.info(msg)


@wtt.spanned()
def example_4_with_an_uncaught_error() -> None:
    """Print and log simple message."""
    msg = "Hello World! I'm about to raise a FileNotFoundError"
    print(msg)
    logging.info(msg)
    raise FileNotFoundError("My FileNotFoundError message")