]: P[K];
5 | };
6 |
7 | function combine(part1: C, part2: Subtract
): P {
8 | // @ts-ignore (just trust me on this, typescript)
9 | return { ...part1, ...part2 };
10 | }
11 |
12 | /**
13 | * High order component for partially applying react component variables
14 | */
15 | export function partial
>(
16 | Comp: React.ComponentType
,
17 | partial: C
18 | ) {
19 | const Partial: React.FC> = (rest) => {
20 | const props: P = combine(partial, rest);
21 | return ;
22 | };
23 | return Partial;
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/src/utils/protocol.ts:
--------------------------------------------------------------------------------
1 | import { getPublicRuntimeConfig } from "../config";
2 |
3 | /**
4 | * whether or not the browser is connected through HTTPS
5 | */
6 | export const isSecure = () => {
7 | return (
8 | (process.browser &&
9 | window &&
10 | window.location.toString().startsWith("https")) ||
11 | false
12 | );
13 | };
14 |
15 | /**
16 | * whether or not the frontend being self-hosting
17 | * NOTE: this is not secure it's obviously spoofable
18 | * this is just used to change up the view and information
19 | * displayed a little bit to be relevant to the user
20 | */
21 | export const isSelfHosted = () => {
22 | const { endpoint } = getPublicRuntimeConfig();
23 | return endpoint !== "inquest.dev";
24 | };
25 |
26 | /**
27 | * get's the location of the documentation site
28 | * evaluates to https://docs.inquet.dev/docs/overview in production
29 | */
30 | export function getDocsURL() {
31 | const secure = isSecure();
32 | const { docsEndpoint } = getPublicRuntimeConfig();
33 | return `http${secure ? "s" : ""}://${docsEndpoint}`;
34 | }
35 |
36 | /**
37 | * get's the location of the get started docs page
38 | */
39 | export function getGetStartedDocsURL() {
40 | const route = isSelfHosted()
41 | ? "/docs/getting_started_with_docker"
42 | : "/docs";
43 | return getDocsURL() + route;
44 | }
45 |
--------------------------------------------------------------------------------
/frontend/src/utils/types.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // eslint: disable=all
4 | export type PropsOf<
5 | F extends React.ComponentType
6 | > = F extends React.ComponentType ? P : never;
7 |
8 | export type Dictionary = { [key: string]: V };
9 | export type SparseArray = { [key: number]: V };
10 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require("tailwindcss/defaultTheme");
2 |
3 | module.exports = {
4 | purge: false,
5 | theme: {
6 | extend: {
7 | fontFamily: {
8 | sans: ["Roboto", ...defaultTheme.fontFamily.sans],
9 | },
10 | },
11 | },
12 | variants: {},
13 | plugins: [],
14 | };
15 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "strictNullChecks": true,
16 | "strictPropertyInitialization": true,
17 | "jsx": "preserve"
18 | },
19 | "types": ["ace"],
20 | "exclude": ["node_modules"],
21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
22 | "typeRoots": ["src/types", "node_modules/@types"]
23 | }
24 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | overrides: [
3 | {
4 | files: "*.{ts,js,tsx,jsx,json}",
5 | options: {
6 | tabWidth: 4,
7 | },
8 | },
9 | ],
10 | };
11 |
--------------------------------------------------------------------------------
/probe/Makefile:
--------------------------------------------------------------------------------
1 | # The binary to build (just the basename).
2 | MODULE := inquest
3 |
4 | # Where to push the docker image.
5 | REGISTRY ?= gcr.io/yiblet/inquest
6 |
7 | IMAGE := $(REGISTRY)/$(MODULE)
8 |
9 | # This version-strategy uses git tags to set the version string
10 | TAG := $(shell git describe --tags --always --dirty)
11 |
12 | BLUE='\033[0;34m'
13 | NC='\033[0m' # No Color
14 |
15 | dist:
16 | poetry build
17 |
18 | run:
19 | @python -m $(MODULE)
20 |
21 | test:
22 | @pytest
23 |
24 | fix:
25 | yapf -ir inquest
26 |
27 | lint:
28 | @echo "\n${BLUE}Running Pylint against source and test files...${NC}\n"
29 | @pylint --rcfile=setup.cfg **/*.py
30 | @echo "\n${BLUE}Running yapf against source and test files...${NC}\n"
31 | @yapf -qr inquest
32 |
33 | # Example: make build-prod VERSION=1.0.0
34 | build-prod:
35 | @echo "\n${BLUE}Building Production image with labels:\n"
36 | @echo "name: $(MODULE)"
37 | @echo "version: $(VERSION)${NC}\n"
38 | @sed \
39 | -e 's|{NAME}|$(MODULE)|g' \
40 | -e 's|{VERSION}|$(VERSION)|g' \
41 | docker/prod.Dockerfile | docker build -t $(IMAGE):$(VERSION) -f- .
42 |
43 |
44 | build-dev:
45 | @echo "\n${BLUE}Building Development image with labels:\n"
46 | @echo "name: $(MODULE)"
47 | @echo "version: $(TAG)${NC}\n"
48 | @sed \
49 | -e 's|{NAME}|$(MODULE)|g' \
50 | -e 's|{VERSION}|$(TAG)|g' \
51 | docker/dev.Dockerfile | docker build -t $(IMAGE):$(TAG) -f- .
52 |
53 | set-version:
54 | sed -i 's/^version.*/version = "$(VERSION)"/g' pyproject.toml
55 | sed -i 's/^VERSION.*/VERSION = "$(VERSION)"/g' inquest/utils/version.py
56 |
57 | # Example: make shell CMD="-c 'date > datefile'"
58 | shell: build-dev
59 | @echo "\n${BLUE}Launching a shell in the containerized build environment...${NC}\n"
60 | @docker run \
61 | -ti \
62 | --rm \
63 | --entrypoint /bin/bash \
64 | -u $$(id -u):$$(id -g) \
65 | $(IMAGE):$(TAG) \
66 | $(CMD)
67 |
68 | # Example: make push VERSION=0.0.2
69 | push: build-prod
70 | @echo "\n${BLUE}Pushing image to GitHub Docker Registry...${NC}\n"
71 | @docker push $(IMAGE):$(VERSION)
72 |
73 | version:
74 | @echo $(TAG)
75 |
76 | .PHONY: clean image-clean build-prod push test
77 |
78 | clean:
79 | rm -rf .pytest_cache .coverage .pytest_cache coverage.xml
80 |
81 | docker-clean:
82 | @docker system prune -f --filter "label=name=$(MODULE)"
83 |
--------------------------------------------------------------------------------
/probe/README.md:
--------------------------------------------------------------------------------
1 | # The Inquest Probe
2 |
3 | This subdirectory holds all the python code for inquest.
4 | At it's core this is just a pip installable python library that you have to turn on
5 | somewhere in your codebase to connect to the inquest dashboard.
6 |
7 | ## Installation
8 |
9 | Inquest is verified to work with python3.7 and later. The python installation is just:
10 |
11 | ```shell
12 | pip install inquest
13 | ```
14 |
15 | If you want more information on how to get started with inquest [go here to get started](https://docs.inquest.dev/docs/overview)
16 |
17 |
18 | ## examples
19 |
20 | The example directory has examples for you to try out inquest. All examples follow the same
21 | command line format.
22 |
23 | Pass in your `api_key` through the `-id` flag, and if you're running inquest locally in docker
24 | (with the backend in http://localhost:4000) pass in the `-local` flag.
25 |
26 | Here's a full example for `examples/fibonacci.py`
27 |
28 | ```
29 | python -m examples.fibonacci -local -id 123fake-api-key
30 | ```
31 |
--------------------------------------------------------------------------------
/probe/docker/base.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8.1-buster
2 | RUN apt-get update && apt-get install -y --no-install-recommends --yes vim netcat
3 |
--------------------------------------------------------------------------------
/probe/docker/dev.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.8.1-buster AS builder
2 | RUN apt-get update && apt-get install -y --no-install-recommends --yes python3-venv gcc libpython3-dev && \
3 | python3 -m venv /venv && \
4 | /venv/bin/pip install --upgrade pip
5 |
6 | FROM builder AS builder-venv
7 |
8 | COPY requirements.txt /requirements.txt
9 | RUN /venv/bin/pip install -r /requirements.txt
10 |
11 | FROM builder-venv AS tester
12 |
13 | COPY . /app
14 | WORKDIR /app
15 | RUN /venv/bin/pytest
16 |
17 | FROM martinheinz/python-3.8.1-buster-tools:latest AS runner
18 | COPY --from=tester /venv /venv
19 | COPY --from=tester /app /app
20 |
21 | WORKDIR /app
22 |
23 | ENTRYPOINT ["/venv/bin/python3", "-m", "inquest"]
24 | USER 1001
25 |
26 | LABEL name={NAME}
27 | LABEL version={VERSION}
28 |
--------------------------------------------------------------------------------
/probe/docker/prod.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:buster-slim AS builder
2 | RUN apt-get update && \
3 | apt-get install --no-install-suggests --no-install-recommends --yes python3-venv gcc libpython3-dev && \
4 | python3 -m venv /venv && \
5 | /venv/bin/pip install --upgrade pip
6 |
7 | FROM builder AS builder-venv
8 | COPY requirements.txt /requirements.txt
9 | RUN /venv/bin/pip install --disable-pip-version-check -r /requirements.txt
10 |
11 | FROM builder-venv AS tester
12 |
13 | COPY . /app
14 | WORKDIR /app
15 | RUN /venv/bin/pytest
16 |
17 | FROM gcr.io/distroless/python3-debian10 AS runner
18 | COPY --from=tester /venv /venv
19 | COPY --from=tester /app /app
20 |
21 | WORKDIR /app
22 |
23 | ENTRYPOINT ["/venv/bin/python3", "-m", "inquest"]
24 | USER 1001
25 |
26 | LABEL name={NAME}
27 | LABEL version={VERSION}
--------------------------------------------------------------------------------
/probe/examples/__init__.py:
--------------------------------------------------------------------------------
1 | # test
2 |
--------------------------------------------------------------------------------
/probe/examples/example_installation.py:
--------------------------------------------------------------------------------
1 | import inquest
2 |
3 | # here's an example of setting up inquest on a hello world program
4 | # at it's core all you have to do is just run inquest.enable
5 |
6 |
7 | def main():
8 | inquest.enable(api_key="YOUR API KEY HERE", glob=["*.py"])
9 | # that's it! you there's nothing else to do
10 |
11 | # inquest is now idling in the background and it will listen
12 | # to changes you make on the dashboard and setup log
13 | # statements as you add them
14 | print("hello world")
15 |
16 |
17 | if __name__ == "__main__":
18 | main()
19 |
--------------------------------------------------------------------------------
/probe/examples/fibonacci.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import logging
3 | import logging.config
4 | from time import sleep
5 |
6 | import inquest
7 | from inquest.utils.logging import LOGGING_CONFIG
8 |
9 | logging.config.dictConfig(LOGGING_CONFIG)
10 |
11 |
12 | def fib(value: int):
13 | if value == 0:
14 | return 1
15 | if value == 1:
16 | return 1
17 | sleep(0.5)
18 | return fib(value - 1) + fib(value - 2)
19 |
20 |
21 | def main():
22 | inquest.enable(**cli(), glob=["examples/**/*.py"])
23 | while True:
24 | fib(20)
25 |
26 |
27 | def cli():
28 | parser = argparse.ArgumentParser("inquest example")
29 | parser.add_argument("-id", type=str)
30 | parser.add_argument('-local', action='store_true')
31 | args = parser.parse_args()
32 | result = {'api_key': args.id}
33 | if args.local:
34 | result['host'] = 'localhost'
35 | result['port'] = 4000
36 | return result
37 |
38 |
39 | if __name__ == "__main__":
40 | main()
41 |
--------------------------------------------------------------------------------
/probe/examples/heartbeat.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import logging
3 | import logging.config
4 | from time import sleep
5 |
6 | import inquest
7 | from inquest.utils.logging import LOGGING_CONFIG
8 |
9 | logging.config.dictConfig(LOGGING_CONFIG)
10 |
11 |
12 | def work(value):
13 | return value + 2
14 |
15 |
16 | def main():
17 | inquest.enable(**cli(), glob=["examples/**/*.py"])
18 | value = 0
19 | while True:
20 | sleep(0.2)
21 | value = work(value)
22 |
23 |
24 | def cli():
25 | parser = argparse.ArgumentParser("inquest example")
26 | parser.add_argument("-id", type=str)
27 | parser.add_argument('-local', action='store_true')
28 | args = parser.parse_args()
29 | result = {'api_key': args.id}
30 | if args.local:
31 | result['host'] = 'localhost'
32 | result['port'] = 4000
33 | return result
34 |
35 |
36 | if __name__ == "__main__":
37 | main()
38 |
--------------------------------------------------------------------------------
/probe/inquest/__init__.py:
--------------------------------------------------------------------------------
1 | from inquest.runner import enable
2 |
--------------------------------------------------------------------------------
/probe/inquest/comms/client_consumer.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 |
3 | from gql import Client
4 |
5 |
6 | class ClientConsumer(contextlib.AsyncExitStack):
7 | # whether or not this consumer needs to be run at initialization
8 | initialization = False
9 |
10 | def __init__(self):
11 | super().__init__()
12 | self._client = None
13 | self._trace_set_id = None
14 |
15 | def _set_values(self, client: Client, trace_set_id: str):
16 | self._client = client
17 | self._trace_set_id = trace_set_id
18 |
19 | @property
20 | def trace_set_id(self) -> str:
21 | if self._trace_set_id is None:
22 | raise ValueError('consumer wasn\'t given asccess to the client')
23 | return self._trace_set_id
24 |
25 | @property
26 | def client(self) -> Client:
27 | if self._client is None:
28 | raise ValueError('consumer wasn\'t given asccess to the client')
29 | return self._client
30 |
31 | async def main(self):
32 | raise NotImplementedError()
33 |
--------------------------------------------------------------------------------
/probe/inquest/comms/exception_sender.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from gql import gql
4 | from inquest.comms.client_consumer import ClientConsumer
5 | from inquest.comms.utils import log_result
6 | from inquest.utils.exceptions import MultiTraceException, ProbeException
7 |
8 | LOGGER = logging.getLogger(__name__)
9 |
10 |
11 | class ExceptionSender(ClientConsumer):
12 |
13 | initialization = True
14 |
15 | def __init__(self,):
16 | super().__init__()
17 | self.query = gql(
18 | '''
19 | mutation ProbeFailureMutation($input: NewProbeFailureInput!) {
20 | newProbeFailure(newProbeFailure: $input) {
21 | message
22 | }
23 | }
24 | '''
25 | )
26 |
27 | async def _send_exception(self, exception: ProbeException):
28 | LOGGER.debug(
29 | 'sending exception="%s" trace_id="%s"',
30 | exception.message,
31 | getattr(exception, 'trace_id', None),
32 | )
33 | result = (
34 | await self.client.execute(
35 | self.query,
36 | variable_values={
37 | 'input':
38 | {
39 | 'message': str(exception.message),
40 | 'traceId': exception.trace_id,
41 | }
42 | }
43 | )
44 | )
45 |
46 | async def send_exception(self, exception: Exception):
47 | if isinstance(exception, MultiTraceException):
48 | errors: MultiTraceException = exception
49 | for error in errors.errors.items():
50 | if not isinstance(error, ProbeException):
51 | error = ProbeException(message=str(error))
52 | await self._send_exception(error)
53 | elif isinstance(exception, ProbeException):
54 | await self._send_exception(exception)
55 | else:
56 | await self._send_exception(ProbeException(message=str(exception)))
57 |
58 | async def main(self):
59 | pass
60 |
--------------------------------------------------------------------------------
/probe/inquest/comms/heartbeat.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 |
4 | from gql import gql
5 |
6 | from inquest.comms.client_consumer import ClientConsumer
7 | from inquest.comms.utils import wrap_log
8 |
9 | LOGGER = logging.getLogger(__name__)
10 |
11 |
12 | class Heartbeat(ClientConsumer):
13 | """
14 | Periodically polls the backend to tell it it's still alive
15 | """
16 |
17 | def __init__(self, *, delay: int = 60):
18 | super().__init__()
19 | self.delay = delay
20 | self.query = gql(
21 | """\
22 | mutation HeartbeatMutation {
23 | heartbeat {
24 | isAlive
25 | }
26 | }
27 | """
28 | )
29 |
30 | async def _send_heartbeat(self):
31 | return (await self.client.execute(self.query))
32 |
33 | async def main(self):
34 | while True:
35 | LOGGER.debug("heartbeat")
36 | await wrap_log(LOGGER, self._send_heartbeat(), mute_error=True)
37 | await asyncio.sleep(self.delay)
38 |
--------------------------------------------------------------------------------
/probe/inquest/comms/module_sender.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import List, Optional, Union
3 |
4 | from gql import gql
5 | from inquest.comms.client_consumer import ClientConsumer
6 | from inquest.comms.utils import wrap_log
7 | from inquest.file_module_resolver import get_root_dir
8 | from inquest.file_sender import FileSender
9 | from inquest.module_tree import FileInfo, ModuleTree
10 |
11 | LOGGER = logging.getLogger(__name__)
12 |
13 |
14 | class ModuleSender(ClientConsumer):
15 | initialization = True
16 |
17 | def __init__(
18 | self,
19 | *,
20 | url: str,
21 | glob: Union[str, List[str]],
22 | exclude: Optional[List[str]] = None,
23 | ):
24 | super().__init__()
25 | self.sender = FileSender(url)
26 | self.root_dir = get_root_dir()
27 | self.glob = glob
28 | self.exclude = exclude
29 | self.query = gql(
30 | """
31 | mutation NewFileContentMutation($input: FileContentInput!) {
32 | newFileContent(fileInput: $input) {
33 | name
34 | }
35 | }
36 | """
37 | )
38 |
39 | async def __aenter__(self):
40 | await super().__aenter__()
41 | await self.enter_async_context(self.sender)
42 | return self
43 |
44 | async def _send_module(self, module: FileInfo, file_id: str):
45 | params = {
46 | "input": {
47 | "fileId": file_id,
48 | **module.encode()
49 | },
50 | }
51 | LOGGER.debug("input params %s", params)
52 | await wrap_log(
53 | LOGGER, self.client.execute(self.query, variable_values=params)
54 | )
55 |
56 | async def main(self):
57 | """
58 | sends the modules out
59 | """
60 | LOGGER.info("sending modules")
61 | module_tree = ModuleTree(self.root_dir, self.glob, self.exclude)
62 | modules = {
63 | module.name: module
64 | for module in module_tree.modules()
65 | if module.name # filters out modules with no known file
66 | }
67 |
68 | modified_modules = await self.sender.check_hashes(
69 | self.trace_set_id,
70 | [
71 | (module.name, module.absolute_name)
72 | for module in modules.values()
73 | ],
74 | )
75 |
76 | async for module_name, file_id in self.sender.send_files(
77 | trace_set_id=self.trace_set_id,
78 | filenames=list((name, modules[name].absolute_name)
79 | for name in modified_modules),
80 | ):
81 | LOGGER.debug("sending module", extra={"module_name": module_name})
82 | await self._send_module(modules[module_name], file_id)
83 |
84 | LOGGER.info("modules finished being sent")
85 |
--------------------------------------------------------------------------------
/probe/inquest/comms/utils.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Awaitable, Optional, OrderedDict
3 |
4 | from gql.transport.exceptions import TransportQueryError
5 |
6 |
7 | def log_result(logger: logging.Logger, result: OrderedDict):
8 | logger.debug(
9 | "backend returned with data",
10 | extra={
11 | 'data': result,
12 | },
13 | )
14 |
15 |
16 | def log_error(logger: logging.Logger, error: TransportQueryError):
17 | logger.debug(
18 | "backend returned with error",
19 | extra={
20 | 'error': error,
21 | },
22 | )
23 |
24 |
25 | async def wrap_log(
26 | logger: logging.Logger,
27 | result: Awaitable[OrderedDict],
28 | mute_error=False
29 | ) -> Optional[OrderedDict]:
30 | try:
31 | res = await result
32 | log_result(logger, res)
33 | return res
34 | except TransportQueryError as err:
35 | log_error(logger, err)
36 | if not mute_error:
37 | raise err
38 | else:
39 | return None
40 |
--------------------------------------------------------------------------------
/probe/inquest/comms/version_checker.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import aiohttp
4 |
5 | from inquest.utils.version import VERSION
6 |
7 | LOGGER = logging.getLogger(__name__)
8 |
9 |
10 | class VersionCheckException(Exception):
11 | pass
12 |
13 |
14 | async def check_version(url: str):
15 | async with aiohttp.ClientSession() as session:
16 | backend_version = await _get_version(session, url)
17 |
18 | backend_semver = _convert_version(backend_version)
19 | probe_semver = _convert_version(VERSION)
20 |
21 | if len(backend_semver) != 3 or len(probe_semver) != 3:
22 | raise ValueError("version is invalid semver")
23 |
24 | if backend_semver[0] != probe_semver[0] or backend_semver[
25 | 1] != probe_semver[1]:
26 | raise VersionCheckException("backend version incompatible")
27 |
28 | if backend_semver[2] != probe_semver[2]:
29 | LOGGER.warning(
30 | 'minor version mismatch',
31 | extra={
32 | 'backend_version': backend_version,
33 | 'probe_version': VERSION
34 | }
35 | )
36 |
37 |
38 | def _convert_version(version: str):
39 | return [int(ver) for ver in version.split(".")]
40 |
41 |
42 | async def _get_version(session: aiohttp.ClientSession, url: str) -> str:
43 | async with session.get(url) as resp:
44 | resp: aiohttp.ClientResponse = resp
45 |
46 | if resp.status != 200:
47 | LOGGER.error(
48 | "version check failed",
49 | extra={
50 | 'status_code': resp.status,
51 | 'failure_message': (await resp.text())
52 | }
53 | )
54 | raise VersionCheckException('response failed')
55 |
56 | return (await resp.text()).strip()
57 |
--------------------------------------------------------------------------------
/probe/inquest/file_module_resolver.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import sys
4 |
5 | LOGGER = logging.getLogger(__name__)
6 |
7 |
8 | def get_root_dir():
9 | root_dir = os.path.abspath(os.getcwd())
10 | python_paths = set(os.path.abspath(path) for path in sys.path)
11 | if root_dir not in python_paths:
12 | raise ValueError("root %s is not in python path" % root_dir)
13 | return root_dir
14 |
15 |
16 | class FileModuleResolverException(Exception):
17 |
18 | def __init__(self, message: str):
19 | super().__init__(message)
20 | self.message = message
21 |
22 |
23 | class FileModuleResolver:
24 | """
25 | resolves filenames to module names
26 | the logic:
27 | root defines the maximum subdirectory that can be imported from
28 | """
29 |
30 | def __init__(self, package: str):
31 | self.root_dir = get_root_dir()
32 |
33 | main = os.path.abspath(sys.modules[package].__file__)
34 | if not main.startswith(self.root_dir):
35 | raise ValueError(
36 | "current calling module %s is not inside of root" % package
37 | )
38 |
39 | self.main = main[len(self.root_dir) + 1:]
40 | LOGGER.debug("main is %s", self.main)
41 |
42 | def convert_filename_to_modulename(self, filename: str) -> str:
43 |
44 | if filename == self.main:
45 | return "__main__"
46 | if filename.endswith("/__init__.py"):
47 | # python file is a __init__.py
48 | modname = filename[:-len("/__init__.py")]
49 | modname = modname.replace("/", ".")
50 | elif filename.endswith(".py"):
51 | # filename is a normal python file
52 | modname = filename[:-len(".py")]
53 | modname = modname.replace("/", ".")
54 | else:
55 | raise ValueError("file %s is not a python file" % filename)
56 |
57 | LOGGER.debug(
58 | "converting filename",
59 | extra={
60 | "input_filename": filename,
61 | "output_modulename": modname
62 | },
63 | )
64 |
65 | if sys.modules.get(modname) is None:
66 | raise FileModuleResolverException(
67 | "could not find module %s for file %s with given root %s" %
68 | (modname, filename, self.root_dir)
69 | )
70 |
71 | return modname
72 |
--------------------------------------------------------------------------------
/probe/inquest/injection/code_reassigner.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import types
3 | from typing import Dict
4 |
5 | from inquest.module_tree import FunctionOrMethod
6 | from inquest.utils.has_stack import HasStack
7 |
8 | LOGGER = logging.getLogger(__name__)
9 |
10 |
11 | class CodeReassigner(HasStack):
12 |
13 | def __init__(self):
14 | super().__init__()
15 | self._functions: Dict[FunctionOrMethod, types.CodeType] = {}
16 |
17 | def enter(self):
18 | self._stack.callback(self.revert_all)
19 |
20 | def original_code(self, func: FunctionOrMethod):
21 | if func not in self._functions:
22 | raise ValueError('function was not assigned')
23 | return self._functions[func]
24 |
25 | def assign_function(self, func: FunctionOrMethod, code: types.CodeType):
26 | LOGGER.debug(
27 | 'assigning to function', extra={'function': func.__name__}
28 | )
29 | if func not in self._functions:
30 | self._functions[func] = func.__code__
31 | func.__code__ = code
32 |
33 | def revert_function(self, func: FunctionOrMethod):
34 | LOGGER.debug('reverting function', extra={'function': func.__name__})
35 | if func not in self._functions:
36 | raise ValueError('function was not assigned')
37 | func.__code__ = self._functions[func]
38 |
39 | def revert_all(self):
40 | for func in self._functions:
41 | self.revert_function(func)
42 |
--------------------------------------------------------------------------------
/probe/inquest/logging.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import contextlib
4 | from typing import Set
5 |
6 | # flake8: noqa
7 |
8 | _CALL_BACKS: Set[Callback] = set()
9 |
10 |
11 | class Callback:
12 |
13 | def log(self, value: str):
14 | raise NotImplementedError()
15 |
16 | def error(self, trace_id: str, value: Exception):
17 | raise NotImplementedError()
18 |
19 |
20 | class PrintCallback(Callback):
21 |
22 | def log(self, value: str):
23 | print(value)
24 |
25 | def error(self, trace_id: str, value: Exception):
26 | pass
27 |
28 |
29 | def log(value):
30 | # pylint: disable=all
31 | for callback in _CALL_BACKS:
32 | try:
33 | callback.log(value)
34 | except:
35 | pass
36 |
37 |
38 | def error(id: str, value):
39 | # pylint: disable=all
40 | for callback in _CALL_BACKS:
41 | try:
42 | callback.error(id, value)
43 | except:
44 | pass
45 |
46 |
47 | def add_callback(value):
48 | _CALL_BACKS.add(value)
49 |
50 |
51 | def remove_callback(value):
52 | _CALL_BACKS.remove(value)
53 |
54 |
55 | @contextlib.contextmanager
56 | def with_callback(callback):
57 | add_callback(callback)
58 | try:
59 | yield None
60 | finally:
61 | remove_callback(callback)
62 |
--------------------------------------------------------------------------------
/probe/inquest/resources/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/probe/inquest/resources/.gitkeep
--------------------------------------------------------------------------------
/probe/inquest/test/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/probe/inquest/test/__init__.py
--------------------------------------------------------------------------------
/probe/inquest/test/absolutize_test.py:
--------------------------------------------------------------------------------
1 | from inquest.hotpatch import convert_relative_import_to_absolute_import
2 |
3 | # TODO test if this works for relative imports relative to __main__
4 |
5 |
6 | def test_absolutize():
7 | assert convert_relative_import_to_absolute_import(
8 | '.',
9 | 'package',
10 | ) == 'package'
11 |
12 | assert convert_relative_import_to_absolute_import(
13 | '.module',
14 | 'package',
15 | ) == 'package.module'
16 |
17 | assert convert_relative_import_to_absolute_import(
18 | '..module',
19 | 'package.subpackage',
20 | ) == 'package.module'
21 |
22 | assert convert_relative_import_to_absolute_import(
23 | '..',
24 | 'package.subpackage',
25 | ) == 'package'
26 |
27 | assert convert_relative_import_to_absolute_import(
28 | '..module.submodule',
29 | 'package.subpackage',
30 | ) == 'package.module.submodule'
31 |
--------------------------------------------------------------------------------
/probe/inquest/test/ast_test.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import ast
4 |
5 | import inquest.test.sample as test
6 |
--------------------------------------------------------------------------------
/probe/inquest/test/embed_test_module/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/probe/inquest/test/embed_test_module/__init__.py
--------------------------------------------------------------------------------
/probe/inquest/test/embed_test_module/test_imported_module.py:
--------------------------------------------------------------------------------
1 | # pylint: disable-all
2 | def sample(arg1, arg2):
3 | return arg1 + arg2
4 |
5 |
6 | class SampleClass:
7 |
8 | def sample_method(self, arg1, arg2):
9 | return arg1 + arg2
10 |
11 |
12 | class SampleChildClass(SampleClass):
13 | pass
14 |
--------------------------------------------------------------------------------
/probe/inquest/test/embed_test_module/test_unimported_module.py:
--------------------------------------------------------------------------------
1 | # pylint: disable-all
2 | def sample(arg1, arg2):
3 | return arg1 + arg2
4 |
5 |
6 | class SampleClass:
7 |
8 | def sample_method(self, arg1, arg2):
9 | return arg1 + arg2
10 |
11 |
12 | class SampleChildClass(SampleClass):
13 | pass
14 |
--------------------------------------------------------------------------------
/probe/inquest/test/module_tree_test.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from ..module_tree import ModuleTree
4 |
5 |
6 | def test_on_probe_test_module():
7 | tree = ModuleTree(*os.path.split(__file__))
8 | files = {file.absolute_name for file in tree.modules()}
9 | assert __file__ in files
10 |
11 |
12 | def test_on_sample_module():
13 | sample = os.path.join(os.path.dirname(__file__), "sample.py")
14 | tree = ModuleTree(os.path.dirname(__file__), "sample.py")
15 | files = {file.absolute_name: file for file in tree.modules()}
16 | assert sample in files
17 | assert set(func.name for func in files[sample].functions) == {
18 | 'sample', 'async_sample', 'sample_with_decorator',
19 | 'async_sample_with_decorator'
20 | }
21 |
22 | classes = set(cls.name for cls in files[sample].classes)
23 | methods = set(
24 | f'{cls.name}.{met.name}' for cls in files[sample].classes
25 | for met in cls.methods
26 | )
27 | assert classes == {'TestClass', 'TestClassWithDecorator'}
28 | assert methods == {
29 | 'TestClassWithDecorator.async_sample',
30 | 'TestClassWithDecorator.async_sample_with_decorator',
31 | 'TestClassWithDecorator.sample_with_decorator',
32 | 'TestClassWithDecorator.sample',
33 | 'TestClass.async_sample',
34 | 'TestClass.async_sample_with_decorator',
35 | 'TestClass.sample_with_decorator',
36 | 'TestClass.sample',
37 | }
38 |
--------------------------------------------------------------------------------
/probe/inquest/test/probe_test_module/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/probe/inquest/test/probe_test_module/__init__.py
--------------------------------------------------------------------------------
/probe/inquest/test/probe_test_module/test_imported_module.py:
--------------------------------------------------------------------------------
1 | def sample(arg1, arg2):
2 | return arg1 + arg2
3 |
--------------------------------------------------------------------------------
/probe/inquest/test/sample.py:
--------------------------------------------------------------------------------
1 | import functools
2 |
3 |
4 | def sample():
5 | pass
6 |
7 |
8 | @functools.lru_cache()
9 | def sample_with_decorator():
10 | pass
11 |
12 |
13 | async def async_sample():
14 | pass
15 |
16 |
17 | @functools.lru_cache()
18 | async def async_sample_with_decorator():
19 | pass
20 |
21 |
22 | class TestClass():
23 |
24 | def sample(self, x):
25 | pass
26 |
27 | @functools.lru_cache()
28 | def sample_with_decorator():
29 | pass
30 |
31 | async def async_sample():
32 | pass
33 |
34 | @functools.lru_cache()
35 | async def async_sample_with_decorator():
36 | pass
37 |
38 |
39 | @functools.lru_cache()
40 | class TestClassWithDecorator():
41 |
42 | def sample():
43 | pass
44 |
45 | @functools.lru_cache()
46 | def sample_with_decorator():
47 | pass
48 |
49 | async def async_sample():
50 | pass
51 |
52 | @functools.lru_cache()
53 | async def async_sample_with_decorator():
54 | pass
55 |
--------------------------------------------------------------------------------
/probe/inquest/test/test_codegen.py:
--------------------------------------------------------------------------------
1 | import types
2 |
3 | import inquest.injection.codegen as codegen
4 |
5 |
6 | class FakeCodeReassigner:
7 |
8 | def assign_function(self, func, code: types.CodeType):
9 | if func not in self._functions:
10 | self._functions[func] = func.__code__
11 | func.__code__ = code
12 |
13 |
14 | def assign_function(self, func, code: types.CodeType):
15 | if func not in self._functions:
16 | self._functions[func] = func.__code__
17 | func.__code__ = code
18 |
19 |
20 | def test_on_code_reassigner(capsys):
21 | result = codegen.add_log_statements(
22 | FakeCodeReassigner.assign_function,
23 | [codegen.Trace(lineno=9, statement="test", id="test")]
24 | )
25 | assert isinstance(result, types.CodeType)
26 |
27 |
28 | def test_on_basic_assign_function(capsys):
29 | result = codegen.add_log_statements(
30 | assign_function,
31 | [codegen.Trace(lineno=14, statement="test", id="test")]
32 | )
33 | assert isinstance(result, types.CodeType)
34 |
--------------------------------------------------------------------------------
/probe/inquest/utils/chunk.py:
--------------------------------------------------------------------------------
1 | from typing import Iterable, List, TypeVar
2 |
3 | A = TypeVar('A')
4 |
5 |
6 | def chunk(iterable: Iterable[A], size: int) -> Iterable[List[A]]:
7 | if size <= 0:
8 | raise ValueError("size must be positive")
9 | output = []
10 | for val in iterable:
11 | output.append(val)
12 | if len(output) == size:
13 | yield output
14 | output = []
15 | if len(output) != 0:
16 | yield output
17 |
--------------------------------------------------------------------------------
/probe/inquest/utils/exceptions.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Optional
2 |
3 |
4 | class ProbeException(Exception):
5 |
6 | def __init__(self, *, message: str, trace_id: Optional[str] = None):
7 | super().__init__(message, trace_id)
8 | self.trace_id = trace_id
9 | self.message = message
10 |
11 |
12 | class MultiTraceException(Exception):
13 |
14 | def __init__(self, errors: Dict[str, Exception]):
15 | self.errors = errors
16 | super().__init__(errors)
17 |
--------------------------------------------------------------------------------
/probe/inquest/utils/has_stack.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 |
3 |
4 | class HasStack:
5 | """
6 | Utility class to run a an ExitStack in the background
7 | """
8 |
9 | def __init__(self):
10 | self._stack = contextlib.ExitStack()
11 |
12 | def __enter__(self):
13 | self._stack.__enter__()
14 | self.enter()
15 | return self
16 |
17 | def enter(self):
18 | """
19 | this defines the context dependencies and what
20 | needs to be destructured on exit
21 | """
22 | raise NotImplementedError('not implemented')
23 |
24 | def __exit__(self, *exc_details):
25 | return self._stack.__exit__(*exc_details)
26 |
--------------------------------------------------------------------------------
/probe/inquest/utils/logging.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 |
4 | class ExtraFormatter(logging.Formatter):
5 | dummy = logging.LogRecord(None, None, None, None, None, None, None)
6 |
7 | def format(self, record):
8 | extra_txt = '\t'
9 | prev = False
10 | for k, v in record.__dict__.items():
11 | if k not in self.dummy.__dict__:
12 | if prev:
13 | extra_txt += ', '
14 | extra_txt += '{}={}'.format(k, v)
15 | prev = True
16 | message = super().format(record)
17 | return message + extra_txt
18 |
19 |
20 | LOGGING_CONFIG = {
21 | 'version': 1,
22 | 'disable_existing_loggers': True,
23 | 'formatters': {
24 | 'standard': {
25 | 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s',
26 | 'class': 'inquest.utils.logging.ExtraFormatter',
27 | },
28 | },
29 | 'handlers': {
30 | 'default': {
31 | 'level': 'DEBUG',
32 | 'formatter': 'standard',
33 | 'class': 'logging.StreamHandler',
34 | 'stream': 'ext://sys.stdout', # Default is stderr
35 | },
36 | },
37 | 'loggers': {
38 | '': { # root logger
39 | 'handlers': ['default'],
40 | 'level': 'WARNING',
41 | 'propagate': False
42 | },
43 | 'inquest': {
44 | 'handlers': ['default'],
45 | 'level': 'DEBUG',
46 | 'propagate': False
47 | },
48 | '__main__': { # if __name__ == '__main__'
49 | 'handlers': ['default'],
50 | 'level': 'DEBUG',
51 | 'propagate': False
52 | },
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/probe/inquest/utils/version.py:
--------------------------------------------------------------------------------
1 | VERSION = "0.4.4"
2 |
--------------------------------------------------------------------------------
/probe/output.profile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/probe/output.profile
--------------------------------------------------------------------------------
/probe/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "inquest"
3 | version = "0.4.4"
4 | description = ""
5 | authors = ["Shalom Yiblet "]
6 | license = "LGPL-3.0+"
7 |
8 | [tool.poetry.dependencies]
9 | python = "^3.7"
10 | pandas = "^1.0.3"
11 | aiohttp = "^3.6.2"
12 | janus = "^0.5.0"
13 | gql = { version = '3.0.0a0', allow-prereleases = true }
14 |
15 | [tool.poetry.dev-dependencies]
16 | pytest-asyncio = "^0.11.0"
17 | pytest = "^5.4.1"
18 | pytest-cov = "^2.8.1"
19 | astpretty = "^2.0.0"
20 | astor = "^0.8.1"
21 |
22 | [build-system]
23 | requires = ["poetry>=0.12"]
24 | build-backend = "poetry.masonry.api"
25 |
--------------------------------------------------------------------------------
/probe/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = --color=yes --cov=inquest --cov-report=xml --cov-report=term -ra
3 | filterwarnings =
4 | log_cli = 1
5 | log_cli_level = INFO
6 | log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
7 | log_cli_date_format = %Y-%m-%d %H:%M:%S
--------------------------------------------------------------------------------
/probe/util/linters.requirements.txt:
--------------------------------------------------------------------------------
1 | pylint
2 | flake8
3 | bandit
4 |
--------------------------------------------------------------------------------
/static/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/static/example.gif
--------------------------------------------------------------------------------