├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backend ├── .dockerignore ├── .eslintrc.js ├── .prettierignore ├── Makefile ├── README.md ├── docker │ ├── base.Dockerfile │ ├── dev.Dockerfile │ └── prod.Dockerfile ├── jest.config.js ├── ormconfig.js ├── package.json ├── schema.graphql ├── scripts │ ├── make_entities.py │ └── make_resolvers.py ├── src │ ├── __tests__ │ │ ├── test_login.ts │ │ └── user_tests.ts │ ├── app.ts │ ├── commands │ │ ├── output_schema.ts │ │ └── serve.ts │ ├── config.ts │ ├── connect.ts │ ├── context.ts │ ├── entities │ │ ├── code │ │ │ ├── class_info.ts │ │ │ ├── directory_info.ts │ │ │ ├── file_info.ts │ │ │ └── function_info.ts │ │ ├── index.ts │ │ ├── organization.ts │ │ ├── probe.ts │ │ ├── probe_failure.ts │ │ ├── trace │ │ │ ├── trace.ts │ │ │ ├── trace_log.ts │ │ │ ├── trace_log_status.ts │ │ │ └── trace_set.ts │ │ └── user.ts │ ├── env.ts │ ├── generated │ │ ├── DeleteTrace.ts │ │ ├── NewProbeMutation.ts │ │ ├── NewTrace.ts │ │ ├── NewTraceSet.ts │ │ ├── NewTraceWithState.ts │ │ ├── ProbeQuery.ts │ │ ├── UpdateTrace.ts │ │ └── globalTypes.ts │ ├── helpers.ts │ ├── logging.ts │ ├── middlewares.ts │ ├── prelude.ts │ ├── repositories │ │ ├── __tests__ │ │ │ └── directory_info_repository_test.ts │ │ ├── directory_info_repository.ts │ │ ├── file_info_repository.ts │ │ ├── probe_failure_repository.ts │ │ ├── probe_repository.ts │ │ └── trace_log_repository.ts │ ├── resolvers │ │ ├── __tests__ │ │ │ ├── code_resolver_test.ts │ │ │ ├── dependency_injection_test.ts │ │ │ ├── probe_test.ts │ │ │ ├── trace_test.ts │ │ │ └── trace_update_test.ts │ │ ├── code_resolver.ts │ │ ├── file_resolver.ts │ │ ├── index.ts │ │ ├── live_tail_resolver.ts │ │ ├── probe_failure_resolver.ts │ │ ├── probe_resolver.ts │ │ ├── trace_resolver.ts │ │ ├── trace_set_resolver.ts │ │ └── user_resolver.ts │ ├── services │ │ ├── auth.ts │ │ ├── storage.ts │ │ └── upload.ts │ ├── topics.ts │ ├── utils │ │ ├── __tests__ │ │ │ └── transaction_test.ts │ │ ├── index.ts │ │ └── testing.ts │ └── version.ts ├── tsconfig.json └── yarn.lock ├── docker-compose.env ├── docker-compose.yml ├── docs ├── .dockerignore ├── .gitignore ├── Makefile ├── README.md ├── babel.config.js ├── docker │ ├── dev.Dockerfile │ └── prod.Dockerfile ├── docs │ ├── examples │ │ ├── mdx.md │ │ └── reference.md │ ├── getting_started.md │ ├── getting_started_with_docker.md │ ├── logs_dont_appear.md │ ├── overview.md │ ├── running_inquest.md │ └── yellow_warning.md ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.js │ │ └── styles.module.css ├── static │ ├── .nojekyll │ └── img │ │ ├── undraw_To_the_stars_qhyy.svg │ │ ├── undraw_developer_activity_bv83.svg │ │ └── undraw_publish_article_icso.svg └── yarn.lock ├── frontend ├── .dockerignore ├── .env ├── .env.development ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .storybook │ ├── config.js │ └── webpack.config.js ├── Makefile ├── README.md ├── apollo.config.js ├── docker │ ├── dev.Dockerfile │ └── prod.Dockerfile ├── jest.config.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public │ └── resources │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── inquest.png │ │ ├── inquest_logging_video.webm │ │ └── site.webmanifest ├── schema.graphql ├── src │ ├── components │ │ ├── code_view │ │ │ ├── code_view.tsx │ │ │ ├── marker.tsx │ │ │ ├── node_renderer.tsx │ │ │ ├── traces.tsx │ │ │ └── utils.tsx │ │ ├── file_tree.connector.tsx │ │ ├── file_tree │ │ │ ├── file_tree.tsx │ │ │ └── module.tsx │ │ ├── live_tail │ │ │ └── live_tail.tsx │ │ ├── modals │ │ │ └── modal_context.tsx │ │ └── utils │ │ │ ├── labelled_field.tsx │ │ │ ├── layout.tsx │ │ │ ├── notifications.tsx │ │ │ ├── tooltip.tsx │ │ │ └── with_title.tsx │ ├── config.ts │ ├── connectors │ │ ├── code_view.connector.tsx │ │ └── live_tail.connector.tsx │ ├── generated │ │ ├── CodeViewFragment.ts │ │ ├── CodeViewQuery.ts │ │ ├── DeleteTraceMutation.ts │ │ ├── DirectoryFragment.ts │ │ ├── FileFragment.ts │ │ ├── FileTreeFragment.ts │ │ ├── FunctionFragment.ts │ │ ├── LiveProbesFragment.ts │ │ ├── LiveTailSubscription.ts │ │ ├── NewTraceMutation.ts │ │ ├── RemoveRootDirectoryMutation.ts │ │ ├── SubdirectoryFragment.ts │ │ ├── SubdirectoryQuery.ts │ │ ├── TraceFragment.ts │ │ ├── UpdateTraceMutation.ts │ │ ├── UserContextQuery.ts │ │ └── globalTypes.ts │ ├── pages │ │ ├── _app.tsx │ │ ├── dashboard.tsx │ │ ├── index.tsx │ │ ├── login.tsx │ │ ├── privacy_policy.tsx │ │ ├── signup.tsx │ │ └── terms_of_service.tsx │ ├── services │ │ └── ga_service.ts │ ├── styles │ │ ├── article.css │ │ └── style.css │ └── utils │ │ ├── __tests__ │ │ └── collections_tests.ts │ │ ├── apollo_client.ts │ │ ├── auth.tsx │ │ ├── clipboard_copy.ts │ │ ├── collections.ts │ │ ├── debounce.ts │ │ ├── logger.ts │ │ ├── observable.ts │ │ ├── partial.tsx │ │ ├── protocol.ts │ │ └── types.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock ├── prettier.config.js ├── probe ├── Makefile ├── README.md ├── docker │ ├── base.Dockerfile │ ├── dev.Dockerfile │ └── prod.Dockerfile ├── examples │ ├── __init__.py │ ├── example_installation.py │ ├── fibonacci.py │ └── heartbeat.py ├── inquest │ ├── __init__.py │ ├── comms │ │ ├── client_consumer.py │ │ ├── client_provider.py │ │ ├── exception_sender.py │ │ ├── heartbeat.py │ │ ├── log_sender.py │ │ ├── module_sender.py │ │ ├── trace_set_subscriber.py │ │ ├── utils.py │ │ └── version_checker.py │ ├── file_module_resolver.py │ ├── file_sender.py │ ├── hotpatch.py │ ├── injection │ │ ├── ast_injector.py │ │ ├── code_reassigner.py │ │ └── codegen.py │ ├── logging.py │ ├── module_tree.py │ ├── probe.py │ ├── resources │ │ └── .gitkeep │ ├── runner.py │ ├── test │ │ ├── __init__.py │ │ ├── absolutize_test.py │ │ ├── ast_injector_test.py │ │ ├── ast_test.py │ │ ├── diff_test.py │ │ ├── embed_test_module │ │ │ ├── __init__.py │ │ │ ├── test_imported_module.py │ │ │ └── test_unimported_module.py │ │ ├── module_tree_test.py │ │ ├── probe_test.py │ │ ├── probe_test_module │ │ │ ├── __init__.py │ │ │ └── test_imported_module.py │ │ ├── sample.py │ │ └── test_codegen.py │ └── utils │ │ ├── chunk.py │ │ ├── exceptions.py │ │ ├── has_stack.py │ │ ├── logging.py │ │ └── version.py ├── output.profile ├── pyproject.toml ├── pytest.ini ├── setup.cfg └── util │ └── linters.requirements.txt └── static └── example.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | poetry.lock 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | coverage/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | 82 | # virtualenv 83 | .venv/ 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | .idea/ 94 | .pytest_cache/ 95 | 96 | # Confidential 97 | credentials.* 98 | 99 | # Javascript 100 | node_modules/ 101 | .config.json 102 | .config.json.backup 103 | package-lock.json 104 | .next/ 105 | 106 | config.env 107 | 108 | # certs 109 | *.srl 110 | *.csr 111 | *.key 112 | *.pem 113 | secrets.env 114 | secrets.configmap.yml 115 | k8s/ 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What's Inquest? 2 | 3 | Inquest is a logging tool for python programs. It let's you add logs to your running python programs without restarting the program, redeploying the program, or modifying the code in any way. Inquest takes extremely low overhead: the part that's a python library is completely idle unless there is something to log. Inquest is specifically designed to enable you to quickly introspect into Python even in production environments. 4 | 5 | Here's a gif of the magic. I'm running a single python instance in the background and I use Inquest to dynamically add log statements to the running code. 6 | 7 | 8 | ## Installation 9 | 10 | There are two ways to use Inquest: 11 | 12 | 1. [A simple docker-compose](https://docs.inquest.dev/docs/getting_started_with_docker) 13 | 2. [The Inquest Cloud](https://inquest.dev) 14 | 15 | ## Resources 16 | 17 | - [Documentation](https://docs.inquest.dev/docs/overview) 18 | - [Troubleshooting](https://docs.inquest.dev/docs/logs_dont_appear) 19 | - [Slack Group](https://join.slack.com/t/inquestcommunity/shared_invite/zt-fq7lra68-nems8~EkICvgf6xRW_J3eg) 20 | - [Bug Tracker](https://github.com/yiblet/inquest/issues) 21 | 22 | ## Frequently Asked Questions 23 | 24 | ### How does Inquest work? 25 | 26 | Inquest works by bytecode injection. The library sets up a connection with the backend. When you add a new 27 | log statment on the dashboard, the backend relays that change the connected python instance. Inside python, 28 | inquest finds the affected functions inside the VM. 29 | 30 | Then it uses the python interpreter to recompile a newly generated piece of python bytecode with the new 31 | log statements inserted. Then inquest pointer-swaps the new bytecode with the old bytecode. 32 | 33 | This has 4 benefits: 34 | 35 | 1. your underlying code object is never modified 36 | 2. reverting a log statement is always possible and will always result in code behavior identical to the original 37 | 3. it generates extremely efficient python 38 | 4. it has not overhead 39 | 40 | ### Why can't I edit the code in the dashboard? 41 | 42 | The dashboard right now cannot persist changes to the files. So modifications to the file on the dashboard 43 | wouldn't have been reflected on the underlying file. This opens up an avenue for gotchas where you unintetionally change the IDE but never see those changes in your VCS. In order to avoid that, we made things, simple. The code viewer is read-only. 44 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | coverage/ 4 | -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true, 6 | node: true, 7 | }, 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | ecmaFeatures: { 11 | experimentalObjectRestSpread: true, 12 | jsx: true, 13 | }, 14 | sourceType: "module", 15 | }, 16 | plugins: ["@typescript-eslint"], 17 | extends: [ 18 | "eslint:recommended", 19 | "plugin:@typescript-eslint/eslint-recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | ], 22 | rules: { 23 | "linebreak-style": [2, "unix"], 24 | quotes: [2, "double"], 25 | "@typescript-eslint/no-unused-vars": [1, { args: "none" }], 26 | "@typescript-eslint/explicit-function-return-type": "off", 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /backend/.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | dist 6 | src/generated 7 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # The Inquest Backend 2 | 3 | Commands: 4 | 5 | ``` 6 | yarn run build 7 | ``` 8 | 9 | compiles the typescript 10 | 11 | ``` 12 | yarn run start 13 | ``` 14 | 15 | compiles the typescript and starts the server 16 | 17 | ``` 18 | yarn run test 19 | ``` 20 | 21 | runs tests 22 | 23 | ``` 24 | yarn run schema 25 | ``` 26 | 27 | generates graphql schema 28 | 29 | ``` 30 | yarn run codegen 31 | ``` 32 | 33 | generates apollo client types (used for testing) 34 | -------------------------------------------------------------------------------- /backend/docker/base.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.12-buster 2 | -------------------------------------------------------------------------------- /backend/docker/dev.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.12-buster AS builder 2 | WORKDIR /app 3 | COPY package.json package.json 4 | COPY yarn.lock yarn.lock 5 | RUN yarn install 6 | 7 | FROM builder AS tester 8 | WORKDIR /app 9 | COPY . . 10 | RUN make test 11 | 12 | FROM tester AS runner 13 | WORKDIR /app 14 | 15 | ENTRYPOINT ["/usr/local/bin/yarn", "start"] 16 | 17 | LABEL name={NAME} 18 | LABEL version={VERSION} 19 | -------------------------------------------------------------------------------- /backend/docker/prod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.12-buster-slim AS builder 2 | WORKDIR /app 3 | COPY package.json package.json 4 | COPY yarn.lock yarn.lock 5 | RUN yarn install 6 | 7 | FROM builder AS runner 8 | WORKDIR /app 9 | COPY . . 10 | RUN yarn run build 11 | 12 | ENTRYPOINT ["/usr/local/bin/yarn", "start:js"] 13 | 14 | LABEL name={NAME} 15 | LABEL version={VERSION} 16 | -------------------------------------------------------------------------------- /backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | collectCoverage: true, 5 | }; 6 | -------------------------------------------------------------------------------- /backend/ormconfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: "postgres", 3 | host: process.env.POSTGRES_HOST || "localhost", 4 | port: parseInt(process.env.POSTGRES_PORT) || 5432, 5 | username: process.env.POSTGRES_USER || "postgres", 6 | password: process.env.POSTGRES_PASSWORD || "postgres", 7 | database: process.env.POSTGRES_DB || "postgres", 8 | synchronize: process.env.BACKEND_SYNCHRONIZE != undefined, 9 | logger: "debug", 10 | cache: true, 11 | entities: [__dirname + "/build/entities/**/*.js"], 12 | migrations: [__dirname + "/build/migration/**/*.js"], 13 | cli: { 14 | migrationsDir: "src/migrations", 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /backend/scripts/make_entities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import glob 5 | import os 6 | 7 | 8 | def camel(snake_str: str): 9 | first, *others = snake_str.split('_') 10 | return ''.join([first.title(), *map(str.title, others)]) 11 | 12 | 13 | def main(): 14 | parser = argparse.ArgumentParser('gen_entities') 15 | parser.add_argument( 16 | 'entities', 17 | type=str, 18 | help="location of the entity directory (usually src/entities)") 19 | 20 | args = parser.parse_args() 21 | entities: str = args.entities 22 | files = [ 23 | file[len(entities):] 24 | for file in sorted(glob.glob(f'{entities}/**/*.ts', recursive=True)) 25 | if '__tests__' not in file and 'test' not in file 26 | and 'index' not in file and 'abstract' not in file 27 | ] 28 | 29 | print("// AUTOGENERATED FROM scripts/make_entities.py DO NOT MODIFY") 30 | for file in files: 31 | basename = os.path.basename(file) 32 | print(f'import {{ {camel(basename[:-3])} }} from ".{file[:-3]}";') 33 | 34 | print("") 35 | print("// export all entities written inside a list") 36 | print('export const ALL_ENTITIES = [') 37 | for file in files: 38 | basename = os.path.basename(file) 39 | print(f' {camel(basename[:-3])},') 40 | print('];') 41 | 42 | print("") 43 | print("// re-export all entities for easy importing") 44 | print("// prettier-ignore") 45 | print('export {') 46 | for file in files: 47 | basename = os.path.basename(file) 48 | print(f' {camel(basename[:-3])},') 49 | print('};') 50 | 51 | 52 | if __name__ == "__main__": 53 | main() 54 | -------------------------------------------------------------------------------- /backend/scripts/make_resolvers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import glob 5 | import os 6 | 7 | 8 | def camel(snake_str: str): 9 | first, *others = snake_str.split('_') 10 | return ''.join([first.title(), *map(str.title, others)]) 11 | 12 | 13 | def main(): 14 | parser = argparse.ArgumentParser('gen_resolvers') 15 | parser.add_argument( 16 | 'resolvers', 17 | type=str, 18 | help="location of the resolver directory (usually src/resolvers)") 19 | 20 | args = parser.parse_args() 21 | resolvers: str = args.resolvers 22 | files = [ 23 | file[len(resolvers):] for file in sorted( 24 | glob.glob(f'{resolvers}/**/*_resolver.ts', recursive=True)) 25 | if '__tests__' not in file 26 | ] 27 | 28 | print("// AUTOGENERATED FROM scripts/make_resolvers.py DO NOT MODIFY") 29 | for file in files: 30 | basename = os.path.basename(file) 31 | print(f'import {{ {camel(basename[:-3])} }} from ".{file[:-3]}";') 32 | 33 | print("") 34 | print('export const ALL_RESOLVERS = [') 35 | for file in files: 36 | basename = os.path.basename(file) 37 | print(f' {camel(basename[:-3])},') 38 | print('];') 39 | 40 | 41 | if __name__ == "__main__": 42 | main() 43 | -------------------------------------------------------------------------------- /backend/src/__tests__/user_tests.ts: -------------------------------------------------------------------------------- 1 | import { User, PasswordValidity } from "../entities/user"; 2 | 3 | test("validate password", () => { 4 | expect(User.validatePassword("testing")).toStrictEqual( 5 | new PasswordValidity(false, [ 6 | "password must be at least 8 characters long", 7 | ]) 8 | ); 9 | expect(User.validatePassword("fasdfa")).toStrictEqual( 10 | new PasswordValidity(false, [ 11 | "password must be at least 8 characters long", 12 | ]) 13 | ); 14 | expect(User.validatePassword("")).toStrictEqual( 15 | new PasswordValidity(false, [ 16 | "password must be at least 8 characters long", 17 | ]) 18 | ); 19 | expect(User.validatePassword("blahblahlbah2@")).toStrictEqual( 20 | new PasswordValidity(true) 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /backend/src/commands/output_schema.ts: -------------------------------------------------------------------------------- 1 | import "../prelude"; 2 | import { buildSchema } from "../connect"; 3 | import { printSchema } from "graphql"; 4 | 5 | async function generateSchema() { 6 | console.log(printSchema(await buildSchema({}))); 7 | } 8 | 9 | generateSchema(); 10 | -------------------------------------------------------------------------------- /backend/src/commands/serve.ts: -------------------------------------------------------------------------------- 1 | import "../prelude"; 2 | import { config } from "../config"; 3 | import { createApp } from "../app"; 4 | import { ProdConnector } from "../connect"; 5 | 6 | async function bootstrap() { 7 | const app = await createApp(new ProdConnector()); 8 | app.listen({ port: config.server.port }, () => { 9 | console.log( 10 | `Server ready at http://${config.server.host}:${config.server.port}` 11 | ); 12 | }); 13 | } 14 | bootstrap(); 15 | -------------------------------------------------------------------------------- /backend/src/config.ts: -------------------------------------------------------------------------------- 1 | import "./env"; 2 | import { version } from "./version"; 3 | 4 | // TODO productionize use a more featured configuration management solution 5 | export const config = { 6 | version: version, 7 | server: { 8 | host: "0.0.0.0", 9 | port: parseInt(process.env.BACKEND_PORT || "4000"), 10 | }, 11 | frontend: { 12 | host: process.env.FRONTEND_HOST || "localhost", 13 | port: parseInt(process.env.FRONTEND_PORT || "3000"), 14 | }, 15 | auth: { 16 | secret: process.env.AUTH_SECRET || "auth_secret", 17 | }, 18 | session: { 19 | secret: 20 | process.env.BACKEND_SESSION_SECRET || 21 | "beeN2AsaGeib4taiJahkoo2bShah9ah", 22 | name: "session_store", 23 | // maxAge is in millisecond we set it to 3 days 24 | maxAge: 1000 * 60 * 60 * 24 * 3, 25 | }, 26 | storage: { 27 | client: { 28 | endPoint: process.env.MINIO_HOST || "127.0.0.1", 29 | port: parseInt(process.env.MINIO_PORT || "9000"), 30 | // just checks if the port is 443 31 | useSSL: parseInt(process.env.MINIO_PORT || "9000") === 443, 32 | accessKey: process.env.MINIO_ACCESS_KEY || "minio", 33 | secretKey: process.env.MINIO_SECRET_KEY || "minio123", 34 | }, 35 | bucket: process.env.MINIO_BUCKET_NAME || "bucket", 36 | region: "us-east-1", 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /backend/src/context.ts: -------------------------------------------------------------------------------- 1 | import { User, Probe, Organization, TraceSet } from "./entities"; 2 | import { PublicError } from "./utils"; 3 | import { Logger } from "winston"; 4 | 5 | export class Context { 6 | private _organization: Organization | undefined; 7 | private _traceSet: TraceSet | undefined; 8 | constructor( 9 | public readonly logger: Logger, 10 | private _user: User | null, 11 | private _probe: Probe | null | "new" 12 | ) {} 13 | 14 | get probe(): Probe { 15 | if (!this._probe || this._probe === "new") 16 | throw new PublicError("probe must be logged in"); 17 | return this._probe; 18 | } 19 | 20 | get user(): User { 21 | if (!this._user) throw new PublicError("user must be logged in"); 22 | return this._user; 23 | } 24 | 25 | async traceSet(): Promise { 26 | if (!this._traceSet) { 27 | const org = await this.organization(); 28 | const traceSets = await org.traceSets; 29 | if (traceSets.length === 0) { 30 | throw Error("traceSet is empty"); 31 | } 32 | this._traceSet = traceSets[0]; 33 | } 34 | return this._traceSet; 35 | } 36 | 37 | async organization(): Promise { 38 | // this._orgization is cached if already queried 39 | if (this._organization) { 40 | return this._organization; 41 | } 42 | 43 | let org: Organization; 44 | if (this._user) { 45 | org = await this._user.organization; 46 | } else if (this._probe && this._probe !== "new") { 47 | org = await (await this._probe.traceSet).organization; 48 | } else { 49 | throw new PublicError("must be logged in"); 50 | } 51 | this._organization = org; 52 | return org; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /backend/src/entities/code/class_info.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "type-graphql"; 2 | import { 3 | Entity, 4 | OneToMany, 5 | Column, 6 | ManyToOne, 7 | PrimaryGeneratedColumn, 8 | CreateDateColumn, 9 | UpdateDateColumn, 10 | Index, 11 | } from "typeorm"; 12 | import { FunctionInfo } from "./function_info"; 13 | import { GraphQLInt } from "graphql"; 14 | import { FileInfo } from "./file_info"; 15 | 16 | /** 17 | * Class 18 | * 19 | * id: string 20 | * 21 | * required fields: 22 | * - fileId 23 | * - name 24 | * - line 25 | */ 26 | @Entity() 27 | @ObjectType() 28 | export class ClassInfo { 29 | @Field({ nullable: false }) 30 | @PrimaryGeneratedColumn("uuid") 31 | readonly id: string; 32 | 33 | @Field({ nullable: false }) 34 | @CreateDateColumn() 35 | readonly createdAt: Date; 36 | 37 | @Field({ nullable: false }) 38 | @UpdateDateColumn() 39 | readonly updatedAt: Date; 40 | 41 | @Field({ nullable: false }) 42 | @Column({ nullable: false }) 43 | readonly name: string; 44 | 45 | @Field((type) => GraphQLInt, { nullable: false }) 46 | @Column({ nullable: false, type: "int" }) 47 | readonly startLine: number; 48 | 49 | @Field((type) => GraphQLInt, { nullable: false }) 50 | @Column({ nullable: false, type: "int" }) 51 | readonly endLine: number; 52 | 53 | @Field((type) => FileInfo, { nullable: false }) 54 | @ManyToOne((type) => FileInfo, { nullable: false, onDelete: "CASCADE" }) 55 | file: Promise; 56 | 57 | @Index() 58 | @Column({ nullable: false }) 59 | fileId: string; 60 | 61 | @Field((type) => [FunctionInfo], { nullable: false }) 62 | @OneToMany((type) => FunctionInfo, (func) => func.parentClass, { 63 | nullable: false, 64 | }) 65 | methods: Promise; 66 | 67 | @Field((type) => ClassInfo, { nullable: true }) 68 | @ManyToOne((type) => ClassInfo, { nullable: true, onDelete: "CASCADE" }) 69 | parentClass: Promise; 70 | 71 | @Column({ nullable: true }) 72 | parentClassId?: string; 73 | 74 | @Field((type) => [ClassInfo], { nullable: false }) 75 | @OneToMany((type) => ClassInfo, (func) => func.parentClass, { 76 | nullable: false, 77 | }) 78 | subClasses: Promise; 79 | } 80 | -------------------------------------------------------------------------------- /backend/src/entities/code/directory_info.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "type-graphql"; 2 | import { 3 | Entity, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | Index, 8 | Column, 9 | ManyToOne, 10 | OneToMany, 11 | } from "typeorm"; 12 | import { TraceSet } from "../trace/trace_set"; 13 | import { FileInfo } from "./file_info"; 14 | import { plainToClass } from "class-transformer"; 15 | 16 | /** 17 | * DirectoryInfo 18 | * 19 | * id: string 20 | * required fields: 21 | * - name 22 | */ 23 | @Entity() 24 | @Index(["name", "traceSetId"], { unique: true }) 25 | @ObjectType() 26 | export class DirectoryInfo { 27 | @Field({ nullable: false }) 28 | @PrimaryGeneratedColumn("uuid") 29 | readonly id: string; 30 | 31 | @Field({ nullable: false }) 32 | @CreateDateColumn() 33 | readonly createdAt: Date; 34 | 35 | @Field({ nullable: false }) 36 | @UpdateDateColumn() 37 | readonly updatedAt: Date; 38 | 39 | @Field({ nullable: false }) 40 | @Column() 41 | readonly name: string; 42 | 43 | @Field((type) => [DirectoryInfo], { nullable: false }) 44 | @OneToMany((type) => DirectoryInfo, (dir) => dir.parentDirectory, { 45 | nullable: false, 46 | }) 47 | subDirectories: Promise; 48 | 49 | @Field((type) => DirectoryInfo, { nullable: true }) 50 | @ManyToOne((type) => DirectoryInfo, { nullable: true, onDelete: "CASCADE" }) 51 | parentDirectory: Promise; 52 | 53 | @Column({ nullable: true }) 54 | parentDirectoryId?: string; 55 | 56 | @Field((type) => [FileInfo], { nullable: false }) 57 | @OneToMany((type) => FileInfo, (dir) => dir.parentDirectory, { 58 | nullable: false, 59 | }) 60 | files: Promise; 61 | 62 | @Field((type) => TraceSet, { nullable: false }) 63 | @ManyToOne((type) => TraceSet, { nullable: false }) 64 | traceSet: Promise; 65 | 66 | @Index() 67 | @Column({ nullable: false }) 68 | traceSetId: string; 69 | 70 | static create(data: { 71 | name: string; 72 | traceSetId: string; 73 | parentDirectoryId?: string; 74 | }) { 75 | return plainToClass(DirectoryInfo, data); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /backend/src/entities/code/file_info.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "type-graphql"; 2 | import { 3 | Entity, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | Index, 8 | Column, 9 | ManyToOne, 10 | } from "typeorm"; 11 | import { TraceSet } from "../trace/trace_set"; 12 | import { DirectoryInfo } from "./directory_info"; 13 | import { plainToClass } from "class-transformer"; 14 | 15 | /** 16 | * FileInfo 17 | * file information 18 | * 19 | * id: string 20 | * required fields: 21 | * - name 22 | * - objectName 23 | * - parentDirectoryId 24 | * - md5sum 25 | * 26 | * TODO count the number of lines on each file 27 | * 28 | */ 29 | @Entity() 30 | @Index(["name", "traceSetId"], { unique: true }) 31 | @ObjectType() 32 | export class FileInfo { 33 | @Field({ nullable: false }) 34 | @PrimaryGeneratedColumn("uuid") 35 | readonly id: string; 36 | 37 | @Field({ nullable: false }) 38 | @CreateDateColumn() 39 | readonly createdAt: Date; 40 | 41 | @Field({ nullable: false }) 42 | @UpdateDateColumn() 43 | readonly updatedAt: Date; 44 | 45 | @Field({ nullable: false }) 46 | @Index() 47 | @Column() 48 | name: string; 49 | 50 | @Field({ nullable: false }) 51 | @Column() 52 | md5sum: string; 53 | 54 | @Column() 55 | objectName: string; 56 | 57 | @Field((type) => DirectoryInfo, { nullable: false }) 58 | @ManyToOne((type) => DirectoryInfo, { 59 | nullable: false, 60 | onDelete: "CASCADE", 61 | }) 62 | parentDirectory: Promise; 63 | 64 | @Column({ nullable: false }) 65 | parentDirectoryId: string; 66 | 67 | @Field((type) => TraceSet, { nullable: false }) 68 | @ManyToOne((type) => TraceSet, { nullable: false }) 69 | traceSet: Promise; 70 | 71 | @Index() 72 | @Column({ nullable: false }) 73 | traceSetId: string; 74 | 75 | static create(data: { 76 | name: string; 77 | objectName: string; 78 | parentDirectoryId: string; 79 | md5sum: string; 80 | traceSetId: string; 81 | }) { 82 | return plainToClass(FileInfo, data); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /backend/src/entities/code/function_info.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "type-graphql"; 2 | import { 3 | Entity, 4 | ManyToOne, 5 | Column, 6 | OneToMany, 7 | PrimaryGeneratedColumn, 8 | CreateDateColumn, 9 | UpdateDateColumn, 10 | Index, 11 | } from "typeorm"; 12 | 13 | import { ClassInfo } from "./class_info"; 14 | import { Trace } from "../trace/trace"; 15 | import { GraphQLInt } from "graphql"; 16 | import { FileInfo } from "./file_info"; 17 | 18 | /** 19 | * Function 20 | * a python function is either part of a module or part of a class 21 | * 22 | * id: string 23 | * 24 | * required fields: 25 | * - fileId 26 | * - name 27 | * - line 28 | */ 29 | @Entity() 30 | @ObjectType() 31 | export class FunctionInfo { 32 | @Field({ nullable: false }) 33 | @PrimaryGeneratedColumn("uuid") 34 | readonly id: string; 35 | 36 | @Field({ nullable: false }) 37 | @CreateDateColumn() 38 | readonly createdAt: Date; 39 | 40 | @Field({ nullable: false }) 41 | @UpdateDateColumn() 42 | readonly updatedAt: Date; 43 | 44 | @Field({ nullable: false }) 45 | @Column({ nullable: false }) 46 | readonly name: string; 47 | 48 | @Field((type) => GraphQLInt, { nullable: false }) 49 | @Column({ nullable: false, type: "int" }) 50 | readonly startLine: number; 51 | 52 | @Field((type) => GraphQLInt, { nullable: false }) 53 | @Column({ nullable: false, type: "int" }) 54 | readonly endLine: number; 55 | 56 | @Field((type) => FileInfo, { nullable: false }) 57 | @ManyToOne((type) => FileInfo, { nullable: false, onDelete: "CASCADE" }) 58 | file: Promise; 59 | 60 | @Index() 61 | @Column({ nullable: false }) 62 | fileId: string; 63 | 64 | @Field((type) => ClassInfo, { nullable: true }) 65 | @ManyToOne((type) => ClassInfo, { nullable: true, onDelete: "CASCADE" }) 66 | parentClass: Promise; 67 | 68 | @Column({ nullable: true }) 69 | parentClassId?: string; 70 | 71 | @Field((type) => [Trace], { nullable: false }) 72 | @OneToMany((type) => Trace, (trace) => trace.function, { 73 | nullable: false, 74 | }) 75 | traces: Promise; 76 | 77 | @Field((type) => Boolean, { nullable: false }) 78 | isMethod() { 79 | return this.parentClassId == null; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /backend/src/entities/index.ts: -------------------------------------------------------------------------------- 1 | // AUTOGENERATED FROM scripts/make_entities.py DO NOT MODIFY 2 | import { ClassInfo } from "./code/class_info"; 3 | import { DirectoryInfo } from "./code/directory_info"; 4 | import { FileInfo } from "./code/file_info"; 5 | import { FunctionInfo } from "./code/function_info"; 6 | import { Organization } from "./organization"; 7 | import { Probe } from "./probe"; 8 | import { ProbeFailure } from "./probe_failure"; 9 | import { Trace } from "./trace/trace"; 10 | import { TraceLog } from "./trace/trace_log"; 11 | import { TraceLogStatus } from "./trace/trace_log_status"; 12 | import { TraceSet } from "./trace/trace_set"; 13 | import { User } from "./user"; 14 | 15 | // export all entities written inside a list 16 | export const ALL_ENTITIES = [ 17 | ClassInfo, 18 | DirectoryInfo, 19 | FileInfo, 20 | FunctionInfo, 21 | Organization, 22 | Probe, 23 | ProbeFailure, 24 | Trace, 25 | TraceLog, 26 | TraceLogStatus, 27 | TraceSet, 28 | User, 29 | ]; 30 | 31 | // re-export all entities for easy importing 32 | // prettier-ignore 33 | export { 34 | ClassInfo, 35 | DirectoryInfo, 36 | FileInfo, 37 | FunctionInfo, 38 | Organization, 39 | Probe, 40 | ProbeFailure, 41 | Trace, 42 | TraceLog, 43 | TraceLogStatus, 44 | TraceSet, 45 | User, 46 | }; 47 | -------------------------------------------------------------------------------- /backend/src/entities/organization.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "type-graphql"; 2 | import { 3 | PrimaryGeneratedColumn, 4 | Column, 5 | Entity, 6 | CreateDateColumn, 7 | UpdateDateColumn, 8 | OneToMany, 9 | } from "typeorm"; 10 | import { TraceSet } from "./trace/trace_set"; 11 | import { User } from "./user"; 12 | import { plainToClass } from "class-transformer"; 13 | 14 | @ObjectType() 15 | @Entity() 16 | export class Organization { 17 | @Column({ nullable: false }) 18 | @PrimaryGeneratedColumn("uuid") 19 | readonly id: string; 20 | 21 | @Field({ nullable: false }) 22 | @CreateDateColumn() 23 | readonly createdAt: Date; 24 | 25 | @Field({ nullable: false }) 26 | @UpdateDateColumn() 27 | readonly updatedAt: Date; 28 | 29 | @Field((type) => [TraceSet], { nullable: false }) 30 | @OneToMany((type) => TraceSet, (traceSet) => traceSet.organization) 31 | traceSets: Promise; 32 | 33 | @Field((type) => [User], { nullable: false }) 34 | @OneToMany((type) => User, (user) => user.organization) 35 | users: Promise; 36 | 37 | static create(data: { name: string }) { 38 | return plainToClass(Organization, data); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/entities/probe.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "type-graphql"; 2 | import { GraphQLBoolean } from "graphql"; 3 | import { 4 | Entity, 5 | PrimaryGeneratedColumn, 6 | Column, 7 | Index, 8 | OneToMany, 9 | ManyToOne, 10 | } from "typeorm"; 11 | 12 | import { TraceSet } from "./trace/trace_set"; 13 | import { TraceLogStatus } from "./trace/trace_log_status"; 14 | import { ProbeFailure } from "./probe_failure"; 15 | /** 16 | * Probe 17 | * 18 | * quick lookup: 19 | * - numeric id 20 | * 21 | * a running instance of an inquest probe 22 | * TODO probes should be connected to users 23 | * TODO probes should list which files are currently being logged 24 | */ 25 | @Entity() 26 | @ObjectType() 27 | export class Probe { 28 | @Field({ nullable: false }) 29 | @PrimaryGeneratedColumn("uuid") 30 | readonly id: string; 31 | 32 | @Field({ nullable: false }) 33 | @Column({ nullable: false }) 34 | lastHeartbeat: Date; 35 | 36 | @Field({ nullable: false }) 37 | @Column({ nullable: false, default: false }) 38 | closed: boolean; 39 | 40 | @Field((type) => GraphQLBoolean, { nullable: false }) 41 | isAlive(): boolean { 42 | // TODO make this smarter 43 | const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000); 44 | return !this.closed && twoMinutesAgo >= this.lastHeartbeat; 45 | } 46 | 47 | @Field((type) => [TraceLogStatus], { nullable: false }) 48 | @OneToMany( 49 | (type) => TraceLogStatus, 50 | (traceLogStatus) => traceLogStatus.probe 51 | ) 52 | traceLogStatuses: Promise; 53 | 54 | @Field((type) => [ProbeFailure], { nullable: false }) 55 | @OneToMany((type) => ProbeFailure, (probeFailure) => probeFailure.probe) 56 | probeFailures: Promise; 57 | 58 | /** 59 | * the respective TraceSet 60 | */ 61 | @Field((type) => TraceSet, { nullable: false }) 62 | @ManyToOne((type) => TraceSet, { nullable: false }) 63 | traceSet: Promise; 64 | 65 | @Index() 66 | @Column({ nullable: false }) 67 | traceSetId: string; 68 | } 69 | -------------------------------------------------------------------------------- /backend/src/entities/probe_failure.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "type-graphql"; 2 | import { 3 | Entity, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | Index, 8 | ManyToOne, 9 | Column, 10 | } from "typeorm"; 11 | 12 | import { Trace } from "./trace/trace"; 13 | import { Probe } from "./probe"; 14 | 15 | /** 16 | * ProbeFailure appears when traces have failures 17 | * probes will report these failures and send them back to the parent 18 | * 19 | * id : number 20 | * initalization requires: 21 | * - message 22 | * - probeId 23 | */ 24 | @Entity() 25 | @ObjectType() 26 | export class ProbeFailure { 27 | @PrimaryGeneratedColumn() 28 | readonly id: number; 29 | 30 | @Field({ nullable: false }) 31 | @CreateDateColumn() 32 | readonly createdAt: Date; 33 | 34 | @Field({ nullable: false }) 35 | @UpdateDateColumn() 36 | readonly updatedAt: Date; 37 | 38 | @Field({ nullable: false }) 39 | @Column({ nullable: false }) 40 | readonly message: string; 41 | 42 | @Field({ nullable: true }) 43 | @Column({ nullable: true }) 44 | traceVersion?: number; 45 | 46 | /** 47 | * the respective Trace 48 | */ 49 | @Field((type) => Trace, { nullable: false }) 50 | @ManyToOne((type) => Trace, { nullable: false }) 51 | trace: Promise; 52 | 53 | @Index() 54 | @Column({ nullable: true }) 55 | traceId?: string; 56 | 57 | /** 58 | * the respective probe 59 | */ 60 | @Field((type) => Probe, { nullable: false }) 61 | @ManyToOne((type) => Probe, { nullable: false }) 62 | probe: Promise; 63 | 64 | @Index() 65 | @Column({ nullable: false }) 66 | probeId: string; 67 | } 68 | -------------------------------------------------------------------------------- /backend/src/entities/trace/trace.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "type-graphql"; 2 | import { 3 | Entity, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | DeleteDateColumn, 8 | Column, 9 | Index, 10 | ManyToOne, 11 | OneToMany, 12 | BeforeUpdate, 13 | } from "typeorm"; 14 | 15 | import { TraceSet } from "./trace_set"; 16 | import { FunctionInfo } from "../code/function_info"; 17 | import { ProbeFailure } from "../probe_failure"; 18 | 19 | /** 20 | * Trace 21 | * a trace models a log statement. A single probe will be enacting multipe different trace statments. 22 | * Traces are immutable in order to always ensure that probes will always hold valid references to trace 23 | * statements. 24 | */ 25 | @Entity() 26 | @ObjectType() 27 | export class Trace { 28 | @Field({ nullable: false }) 29 | @PrimaryGeneratedColumn("uuid") 30 | readonly id: string; 31 | 32 | @Field({ nullable: false }) 33 | @CreateDateColumn() 34 | readonly createdAt: Date; 35 | 36 | @Field({ nullable: false }) 37 | @UpdateDateColumn() 38 | readonly updatedAt: Date; 39 | 40 | @DeleteDateColumn() 41 | readonly deletedAt: Date; 42 | 43 | @Field((type) => FunctionInfo, { nullable: true }) 44 | @ManyToOne((type) => FunctionInfo, { 45 | nullable: true, 46 | onDelete: "SET NULL", 47 | }) 48 | function: Promise; 49 | 50 | @Field({ nullable: true }) 51 | @Index() 52 | @Column({ nullable: true }) 53 | functionId?: string; 54 | 55 | @Field({ nullable: false }) 56 | @Column({ nullable: false }) 57 | statement: string; 58 | 59 | @Field({ nullable: false }) 60 | @Column({ nullable: false, type: "int" }) 61 | line: number; 62 | 63 | @Field({ nullable: false }) 64 | @Column({ nullable: false }) 65 | active: boolean; 66 | 67 | @Field({ nullable: false }) 68 | @Column({ nullable: false, default: 0 }) 69 | version: number; 70 | 71 | @Column({ nullable: false }) 72 | traceSetId: string; 73 | 74 | @Field((type) => TraceSet, { nullable: false }) 75 | @ManyToOne((type) => TraceSet, { nullable: false }) 76 | traceSet: Promise; 77 | 78 | @Field((type) => [ProbeFailure], { nullable: false }) 79 | @OneToMany((type) => ProbeFailure, (probeFailure) => probeFailure.trace) 80 | probeFailures: Promise; 81 | 82 | @BeforeUpdate() 83 | deactivateIfOrphaned() { 84 | if (this.active && this.functionId == null) this.active = false; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /backend/src/entities/trace/trace_log_status.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType, registerEnumType } from "type-graphql"; 2 | import { 3 | Entity, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | Index, 8 | ManyToOne, 9 | Column, 10 | } from "typeorm"; 11 | 12 | import { TraceLog } from "./trace_log"; 13 | import { Probe } from "../probe"; 14 | 15 | // TODO test if this works 16 | export enum TraceLogStatusState { 17 | SENT = 0, 18 | SUCCESS, 19 | ERROR, 20 | } 21 | 22 | registerEnumType(TraceLogStatusState, { 23 | name: "TraceLogStatusState", 24 | description: "the state of the trace log status", 25 | }); 26 | 27 | /** 28 | * TraceLogStatus 29 | * holds information of the result of a tracelog change 30 | * 31 | * when the user adds a new log statement there needs to 32 | * be information tracking the deployment of the change 33 | * of this log statement on the set of probes affected. 34 | * TraceLogStatus is the entity that fulfills that job. 35 | * For each create, update, or delete this log status 36 | * will track whether or not it succeeded for each probe. 37 | */ 38 | @Entity() 39 | @ObjectType() 40 | export class TraceLogStatus { 41 | @Field({ nullable: false }) 42 | @PrimaryGeneratedColumn("uuid") 43 | readonly id: string; 44 | 45 | @Field({ nullable: false }) 46 | @CreateDateColumn() 47 | readonly createdAt: Date; 48 | 49 | @Field({ nullable: false }) 50 | @UpdateDateColumn() 51 | readonly updatedAt: Date; 52 | 53 | @Field((type) => TraceLogStatusState, { nullable: false }) 54 | @Column({ nullable: false, type: "int", default: TraceLogStatusState.SENT }) 55 | type: TraceLogStatusState; 56 | 57 | @Field({ nullable: true }) 58 | @Column({ nullable: true }) 59 | message?: string; 60 | 61 | @Field((type) => Probe, { nullable: false }) 62 | @ManyToOne((type) => Probe, { nullable: false }) 63 | probe: Promise; 64 | 65 | @Index() 66 | @Column({ nullable: false }) 67 | probeId: string; 68 | 69 | @Field((type) => TraceLog, { nullable: false }) 70 | @ManyToOne((type) => TraceLog, { nullable: false }) 71 | traceLog: Promise; 72 | 73 | @Index() 74 | @Column({ nullable: false }) 75 | traceLogId: string; 76 | 77 | static newTraceLogstatus(relations: { 78 | probeId: string; 79 | traceLogId: string; 80 | }): Partial { 81 | return { 82 | type: TraceLogStatusState.SENT, 83 | ...relations, 84 | }; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /backend/src/entities/trace/trace_set.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from "type-graphql"; 2 | import { 3 | Entity, 4 | PrimaryGeneratedColumn, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | ManyToOne, 8 | Column, 9 | } from "typeorm"; 10 | 11 | import { Organization } from "../organization"; 12 | import { plainToClass } from "class-transformer"; 13 | 14 | /** 15 | * TraceSet the current desired set of all active statements 16 | */ 17 | @Entity() 18 | @ObjectType() 19 | export class TraceSet { 20 | @Field({ nullable: false }) 21 | @PrimaryGeneratedColumn("uuid") 22 | readonly id: string; 23 | 24 | @Field({ nullable: false }) 25 | @CreateDateColumn() 26 | readonly createdAt: Date; 27 | 28 | @Field({ nullable: false }) 29 | @UpdateDateColumn() 30 | readonly updatedAt: Date; 31 | 32 | @Field((type) => [Organization], { nullable: false }) 33 | @ManyToOne((type) => Organization, (org) => org.traceSets) 34 | organization: Promise; 35 | 36 | @Column() 37 | organizationId: string; 38 | 39 | static create(data: { organizationId: string }) { 40 | return plainToClass(TraceSet, data); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/src/env.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import { resolve } from "path"; 3 | config({ path: resolve(__dirname, "../.env") }); 4 | -------------------------------------------------------------------------------- /backend/src/generated/DeleteTrace.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: DeleteTrace 8 | // ==================================================== 9 | 10 | export interface DeleteTrace_deleteTrace_function { 11 | readonly __typename: "FunctionInfo"; 12 | readonly name: string; 13 | } 14 | 15 | export interface DeleteTrace_deleteTrace_traceSet_desiredSet_function { 16 | readonly __typename: "FunctionInfo"; 17 | readonly name: string; 18 | } 19 | 20 | export interface DeleteTrace_deleteTrace_traceSet_desiredSet { 21 | readonly __typename: "Trace"; 22 | readonly function: DeleteTrace_deleteTrace_traceSet_desiredSet_function | null; 23 | readonly statement: string; 24 | } 25 | 26 | export interface DeleteTrace_deleteTrace_traceSet { 27 | readonly __typename: "TraceSet"; 28 | readonly id: string; 29 | /** 30 | * the desired set according to this traceSet 31 | */ 32 | readonly desiredSet: ReadonlyArray; 33 | } 34 | 35 | export interface DeleteTrace_deleteTrace { 36 | readonly __typename: "Trace"; 37 | readonly id: string; 38 | readonly function: DeleteTrace_deleteTrace_function | null; 39 | readonly statement: string; 40 | readonly traceSet: DeleteTrace_deleteTrace_traceSet; 41 | } 42 | 43 | export interface DeleteTrace { 44 | readonly deleteTrace: DeleteTrace_deleteTrace; 45 | } 46 | 47 | export interface DeleteTraceVariables { 48 | readonly id: string; 49 | } 50 | -------------------------------------------------------------------------------- /backend/src/generated/NewProbeMutation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: NewProbeMutation 8 | // ==================================================== 9 | 10 | export interface NewProbeMutation_newProbe { 11 | readonly __typename: "Probe"; 12 | readonly id: string; 13 | } 14 | 15 | export interface NewProbeMutation { 16 | readonly newProbe: NewProbeMutation_newProbe; 17 | } 18 | 19 | export interface NewProbeMutationVariables { 20 | readonly id: string; 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/generated/NewTrace.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: NewTrace 8 | // ==================================================== 9 | 10 | export interface NewTrace_newTrace_function { 11 | readonly __typename: "FunctionInfo"; 12 | readonly name: string; 13 | } 14 | 15 | export interface NewTrace_newTrace_traceSet { 16 | readonly __typename: "TraceSet"; 17 | readonly id: string; 18 | } 19 | 20 | export interface NewTrace_newTrace { 21 | readonly __typename: "Trace"; 22 | readonly id: string; 23 | readonly function: NewTrace_newTrace_function | null; 24 | readonly statement: string; 25 | readonly traceSet: NewTrace_newTrace_traceSet; 26 | } 27 | 28 | export interface NewTrace { 29 | readonly newTrace: NewTrace_newTrace; 30 | } 31 | 32 | export interface NewTraceVariables { 33 | readonly functionId: string; 34 | readonly statement: string; 35 | readonly id: string; 36 | readonly line: number; 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/generated/NewTraceSet.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: NewTraceSet 8 | // ==================================================== 9 | 10 | export interface NewTraceSet_newTraceSet { 11 | readonly __typename: "TraceSet"; 12 | readonly id: string; 13 | } 14 | 15 | export interface NewTraceSet { 16 | /** 17 | * creates a traceSet with a given id 18 | */ 19 | readonly newTraceSet: NewTraceSet_newTraceSet; 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/generated/NewTraceWithState.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: NewTraceWithState 8 | // ==================================================== 9 | 10 | export interface NewTraceWithState_newTrace_function { 11 | readonly __typename: "FunctionInfo"; 12 | readonly name: string; 13 | } 14 | 15 | export interface NewTraceWithState_newTrace_traceSet_desiredSet_function { 16 | readonly __typename: "FunctionInfo"; 17 | readonly name: string; 18 | } 19 | 20 | export interface NewTraceWithState_newTrace_traceSet_desiredSet { 21 | readonly __typename: "Trace"; 22 | readonly function: NewTraceWithState_newTrace_traceSet_desiredSet_function | null; 23 | readonly statement: string; 24 | } 25 | 26 | export interface NewTraceWithState_newTrace_traceSet { 27 | readonly __typename: "TraceSet"; 28 | readonly id: string; 29 | /** 30 | * the desired set according to this traceSet 31 | */ 32 | readonly desiredSet: ReadonlyArray; 33 | } 34 | 35 | export interface NewTraceWithState_newTrace { 36 | readonly __typename: "Trace"; 37 | readonly id: string; 38 | readonly function: NewTraceWithState_newTrace_function | null; 39 | readonly statement: string; 40 | readonly traceSet: NewTraceWithState_newTrace_traceSet; 41 | } 42 | 43 | export interface NewTraceWithState { 44 | readonly newTrace: NewTraceWithState_newTrace; 45 | } 46 | 47 | export interface NewTraceWithStateVariables { 48 | readonly functionId: string; 49 | readonly statement: string; 50 | readonly id: string; 51 | readonly line: number; 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/generated/ProbeQuery.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL query operation: ProbeQuery 8 | // ==================================================== 9 | 10 | export interface ProbeQuery_probe { 11 | readonly __typename: "Probe"; 12 | readonly id: string; 13 | } 14 | 15 | export interface ProbeQuery { 16 | readonly probe: ProbeQuery_probe | null; 17 | } 18 | 19 | export interface ProbeQueryVariables { 20 | readonly id: string; 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/generated/UpdateTrace.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: UpdateTrace 8 | // ==================================================== 9 | 10 | export interface UpdateTrace_updateTrace_function { 11 | readonly __typename: "FunctionInfo"; 12 | readonly name: string; 13 | } 14 | 15 | export interface UpdateTrace_updateTrace_traceSet_desiredSet_function { 16 | readonly __typename: "FunctionInfo"; 17 | readonly name: string; 18 | } 19 | 20 | export interface UpdateTrace_updateTrace_traceSet_desiredSet { 21 | readonly __typename: "Trace"; 22 | readonly function: UpdateTrace_updateTrace_traceSet_desiredSet_function | null; 23 | readonly statement: string; 24 | } 25 | 26 | export interface UpdateTrace_updateTrace_traceSet { 27 | readonly __typename: "TraceSet"; 28 | readonly id: string; 29 | /** 30 | * the desired set according to this traceSet 31 | */ 32 | readonly desiredSet: ReadonlyArray; 33 | } 34 | 35 | export interface UpdateTrace_updateTrace { 36 | readonly __typename: "Trace"; 37 | readonly id: string; 38 | readonly function: UpdateTrace_updateTrace_function | null; 39 | readonly statement: string; 40 | readonly traceSet: UpdateTrace_updateTrace_traceSet; 41 | } 42 | 43 | export interface UpdateTrace { 44 | readonly updateTrace: UpdateTrace_updateTrace; 45 | } 46 | 47 | export interface UpdateTraceVariables { 48 | readonly statement?: string | null; 49 | readonly active?: boolean | null; 50 | readonly id: string; 51 | } 52 | -------------------------------------------------------------------------------- /backend/src/generated/globalTypes.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | //============================================================== 7 | // START Enums and Input Objects 8 | //============================================================== 9 | 10 | //============================================================== 11 | // END Enums and Input Objects 12 | //============================================================== 13 | -------------------------------------------------------------------------------- /backend/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { getRepository, getManager, Column, ColumnOptions } from "typeorm"; 2 | 3 | import { User, TraceSet } from "./entities"; 4 | import { hash } from "bcrypt"; 5 | import { Organization } from "./entities/organization"; 6 | import { logger } from "./logging"; 7 | 8 | export async function seedTriple(orgName: string) { 9 | const manager = getManager(); 10 | const orgRepository = getRepository(Organization); 11 | 12 | const organization = await orgRepository.save( 13 | Organization.create({ name: orgName }) 14 | ); 15 | 16 | const user = await manager.save( 17 | User.create({ 18 | email: `${orgName}@example.com`, 19 | firstname: "Test", 20 | lastname: "User", 21 | password: await hash("s#cr3tp4ssw0rd", 10), 22 | organizationId: organization.id, 23 | }) 24 | ); 25 | 26 | const traceSet = await manager.save( 27 | TraceSet.create({ 28 | organizationId: organization.id, 29 | }) 30 | ); 31 | 32 | return { 33 | user, 34 | traceSet, 35 | organization, 36 | }; 37 | } 38 | 39 | export async function seedDatabase() { 40 | const manager = getManager(); 41 | const userRepository = getRepository(User); 42 | const orgRepository = getRepository(Organization); 43 | 44 | const defaultOrganization = await orgRepository.save( 45 | Organization.create({ name: "test" }) 46 | ); 47 | 48 | const defaultUser = User.create({ 49 | email: "default@example.com", 50 | firstname: "Default", 51 | lastname: "User", 52 | password: await hash("s#cr3tp4ssw0rd", 10), 53 | organizationId: defaultOrganization.id, 54 | }); 55 | 56 | await userRepository.save(defaultUser); 57 | 58 | const defaultTraceSet = await manager.save( 59 | TraceSet.create({ 60 | organizationId: defaultOrganization.id, 61 | }) 62 | ); 63 | 64 | logger.info("database seeded", { 65 | user: defaultUser.email, 66 | traceSet: defaultTraceSet.id, 67 | }); 68 | 69 | return { 70 | defaultUser, 71 | }; 72 | } 73 | 74 | export function RelationColumn(options?: ColumnOptions) { 75 | return Column({ nullable: true, ...options }); 76 | } 77 | -------------------------------------------------------------------------------- /backend/src/logging.ts: -------------------------------------------------------------------------------- 1 | import winston = require("winston"); 2 | 3 | function createLogger() { 4 | switch (process.env.NODE_ENV) { 5 | case "test": 6 | return winston.createLogger({ 7 | silent: true, 8 | }); 9 | case "dev": 10 | return winston.createLogger({ 11 | level: "debug", 12 | format: winston.format.prettyPrint(), 13 | defaultMeta: { instance: "backend" }, 14 | transports: [ 15 | new winston.transports.Console({ 16 | format: winston.format.simple(), 17 | }), 18 | ], 19 | }); 20 | case "production": 21 | /* follow through */ 22 | /* eslint-disable */ 23 | default: 24 | /* eslint-enable */ 25 | return winston.createLogger({ 26 | level: "info", 27 | format: winston.format.prettyPrint(), 28 | defaultMeta: { instance: "backend" }, 29 | transports: [ 30 | new winston.transports.Console({ 31 | format: winston.format.simple(), 32 | }), 33 | ], 34 | }); 35 | } 36 | } 37 | 38 | export const logger = createLogger(); 39 | -------------------------------------------------------------------------------- /backend/src/middlewares.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareFn } from "type-graphql"; 2 | import { PublicError } from "./utils"; 3 | import { Context } from "./context"; 4 | 5 | export const ErrorInterceptor: MiddlewareFn = async ( 6 | { context, info }, 7 | next 8 | ) => { 9 | try { 10 | return await next(); 11 | } catch (err) { 12 | if (err instanceof Error) 13 | context.logger.error("exception in graphql resolution", { 14 | operation: info.operation.name?.value || "", 15 | type: info.operation.operation, 16 | parent: info.parentType.name, 17 | field: info.fieldName, 18 | error: err.message ?? "", 19 | stack: err.stack, 20 | }); 21 | else { 22 | context.logger.error("exception in graphql resolution", { 23 | operation: info.operation.name?.value || "", 24 | type: info.operation.operation, 25 | parent: info.parentType.name, 26 | field: info.fieldName, 27 | error: JSON.stringify(err), 28 | }); 29 | } 30 | 31 | if (err instanceof PublicError) throw err; 32 | else throw new Error("an internal error has occured"); 33 | } 34 | }; 35 | 36 | export const LoggingInterceptor: MiddlewareFn = async ( 37 | { context, info }, 38 | next 39 | ) => { 40 | const startTime = Date.now(); 41 | try { 42 | await next(); 43 | } finally { 44 | const endTime = Date.now(); 45 | if (!info.path.prev) 46 | context.logger.info("new operation", { 47 | operation: info.operation.name?.value || "", 48 | requestTime: endTime - startTime, 49 | type: info.operation.operation, 50 | }); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /backend/src/prelude.ts: -------------------------------------------------------------------------------- 1 | // needed for typeorm && type-graphl to function 2 | import "reflect-metadata"; 3 | // imports the .env file 4 | import "./env"; 5 | -------------------------------------------------------------------------------- /backend/src/repositories/__tests__/directory_info_repository_test.ts: -------------------------------------------------------------------------------- 1 | import { DebugConnector } from "../../connect"; 2 | import Container from "typedi"; 3 | import { 4 | getParentDirName, 5 | DirectoryInfoRepository, 6 | } from "../directory_info_repository"; 7 | import { getManager } from "typeorm"; 8 | import { seedTriple } from "../../helpers"; 9 | 10 | beforeAll(async () => { 11 | Container.reset(); 12 | await new DebugConnector().connect(); 13 | }); 14 | 15 | it("test get parentDirectoryectoryName", () => { 16 | expect(getParentDirName("abc/def")).toBe("abc"); 17 | expect(getParentDirName("abc/def/ghij")).toBe("abc/def"); 18 | expect(getParentDirName("abc")).toBe(""); 19 | }); 20 | 21 | it("test generate dirpath", async () => { 22 | const manager = getManager(); 23 | const dirRepo = manager.getCustomRepository(DirectoryInfoRepository); 24 | 25 | const { traceSet } = await seedTriple("org1"); 26 | const obj = await dirRepo.genDirpath("abc/def/ghij", traceSet.id); 27 | const parent1 = await obj.parentDirectory; 28 | const parent2 = parent1 && (await parent1.parentDirectory); 29 | const parent3 = parent2 && (await parent2.parentDirectory); 30 | expect(obj).toMatchObject({ 31 | name: "abc/def/ghij", 32 | }); 33 | expect(parent1).toMatchObject({ 34 | name: "abc/def", 35 | }); 36 | expect(parent2).toMatchObject({ 37 | name: "abc", 38 | }); 39 | expect(parent3).toMatchObject({ 40 | name: "", 41 | }); 42 | 43 | await expect(dirRepo.genRemoveRootDir(traceSet.id)).resolves.toMatchObject({ 44 | id: undefined, 45 | }); 46 | await expect( 47 | dirRepo.findOne({ 48 | name: "", 49 | traceSetId: traceSet.id, 50 | }) 51 | ).resolves.toBeUndefined(); 52 | }); 53 | -------------------------------------------------------------------------------- /backend/src/repositories/directory_info_repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from "typeorm"; 2 | import { DirectoryInfo } from "../entities"; 3 | 4 | export function getParentDirName(path: string): string { 5 | const idx = path.lastIndexOf("/"); 6 | if (idx === -1) { 7 | return ""; 8 | } else { 9 | return path.substring(0, idx); 10 | } 11 | } 12 | 13 | @EntityRepository(DirectoryInfo) 14 | export class DirectoryInfoRepository extends Repository { 15 | /** 16 | * retrieves the list of active probes for the given traceset 17 | */ 18 | 19 | async genRootDir(traceSetId: string) { 20 | const dir = await this.findOne({ 21 | name: "", 22 | traceSetId: traceSetId, 23 | }); 24 | 25 | if (!dir) 26 | return await this.save( 27 | DirectoryInfo.create({ 28 | name: "", 29 | traceSetId: traceSetId, 30 | }) 31 | ); 32 | else return dir; 33 | } 34 | 35 | async genRemoveRootDir(traceSetId: string) { 36 | const dir = await this.findOne({ 37 | name: "", 38 | traceSetId: traceSetId, 39 | }); 40 | if (dir) return await this.remove(dir); 41 | } 42 | 43 | async genDirpath( 44 | dirpath: string, 45 | traceSetId: string 46 | ): Promise { 47 | if (dirpath == "") return await this.genRootDir(traceSetId); 48 | const dir = await this.findOne({ 49 | name: dirpath, 50 | traceSetId: traceSetId, 51 | }); 52 | if (dir) return dir; 53 | const parentDirectory = await this.genDirpath( 54 | getParentDirName(dirpath), 55 | traceSetId 56 | ); 57 | const newDir = DirectoryInfo.create({ 58 | name: dirpath, 59 | traceSetId: traceSetId, 60 | parentDirectoryId: parentDirectory.id, 61 | }); 62 | return await this.save(newDir); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /backend/src/repositories/file_info_repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository, IsNull } from "typeorm"; 2 | import { FileInfo, FunctionInfo, ClassInfo } from "../entities"; 3 | 4 | @EntityRepository(FileInfo) 5 | export class FileInfoRepository extends Repository { 6 | /** 7 | * retrieves the list of active probes for the given traceset 8 | */ 9 | async removeContents(file: FileInfo) { 10 | const [classes, functions] = await Promise.all([ 11 | this.classes(file), 12 | this.functions(file), 13 | ]); 14 | await this.manager.remove([...classes, ...functions]); 15 | } 16 | 17 | /** 18 | * find the subset info the input files that are different compared to 19 | * what's in the database 20 | */ 21 | async findDifferences( 22 | traceSetId: string, 23 | sums: { [file: string]: string } 24 | ) { 25 | const files = await this.createQueryBuilder("file_info") 26 | .where( 27 | // (:...names) spreads the names out 28 | "file_info.name IN (:...names) AND file_info.traceSetId = :traceSetId", 29 | { 30 | names: Object.keys(sums), 31 | traceSetId, 32 | } 33 | ) 34 | .getMany(); 35 | 36 | const fileMap = new Map(); 37 | for (const file of files) { 38 | fileMap.set(file.name, file); 39 | } 40 | 41 | return Object.entries(sums) 42 | .filter(([name, md5sum]) => { 43 | const file = fileMap.get(name); 44 | return !file || file.md5sum !== md5sum; 45 | }) 46 | .map(([name]) => name); 47 | } 48 | 49 | /** 50 | * returns the top level functions available on this file 51 | */ 52 | async functions(file: FileInfo): Promise { 53 | return await this.manager.find(FunctionInfo, { 54 | parentClassId: IsNull(), 55 | fileId: file.id, 56 | }); 57 | } 58 | 59 | /** 60 | * returns the top level classes available on this file 61 | */ 62 | async classes(file: FileInfo): Promise { 63 | return await this.manager.find(ClassInfo, { 64 | fileId: file.id, 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /backend/src/repositories/probe_failure_repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from "typeorm"; 2 | import { ProbeFailure, Probe } from "../entities"; 3 | 4 | @EntityRepository(ProbeFailure) 5 | export class ProbeFailureRepository extends Repository { 6 | /** 7 | * findExistingFailure retrieves the existing failure 8 | */ 9 | async findExistingFailure(probeFailure: ProbeFailure, probe: Probe) { 10 | if (probeFailure.traceId !== undefined) { 11 | return await this.findOne({ 12 | traceId: probeFailure.traceId, 13 | traceVersion: probeFailure.traceVersion, 14 | message: probeFailure.message, 15 | }); 16 | } else { 17 | return await this.createQueryBuilder("failure") 18 | .innerJoin( 19 | "failure.probe", 20 | "probe", 21 | "probe.traceSetId = :traceSetId", 22 | { traceSetId: probe.traceSetId } 23 | ) 24 | .where("failure.message = :message", { 25 | message: probeFailure.message, 26 | }) 27 | .getOne(); 28 | } 29 | } 30 | 31 | /** 32 | * findExistingFailure retrieves the existing failure 33 | */ 34 | buildIncludedTrace() { 35 | return this.createQueryBuilder("failure").innerJoin( 36 | "failure.trace", 37 | "trace", 38 | "probe.traceVersion = trace.version" 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/repositories/probe_repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository, MoreThanOrEqual } from "typeorm"; 2 | import { Probe } from "../entities"; 3 | import { subMinutes } from "date-fns"; 4 | 5 | @EntityRepository(Probe) 6 | export class ProbeRepository extends Repository { 7 | /** 8 | * retrieves the list of active probes for the given traceset 9 | */ 10 | async findActiveProbesIds(traceSetId: string): Promise { 11 | return ( 12 | await this.find({ 13 | select: ["id"], 14 | where: { 15 | traceSetId: traceSetId, 16 | lastHeartbeat: MoreThanOrEqual(subMinutes(new Date(), 5)), 17 | }, 18 | }) 19 | ).map((probe) => probe.id); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/repositories/trace_log_repository.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EntityManager, 3 | EntityRepository, 4 | Repository, 5 | getManager, 6 | } from "typeorm"; 7 | import { TraceLog, TraceLogStatus } from "../entities"; 8 | import { InjectRepository } from "typeorm-typedi-extensions"; 9 | import { ProbeRepository } from "./probe_repository"; 10 | import { PublicError } from "../utils"; 11 | 12 | @EntityRepository(TraceLog) 13 | export class TraceLogRepository extends Repository { 14 | @InjectRepository() 15 | private probeRepository: ProbeRepository; 16 | 17 | /** 18 | * retrieves the list of active probes for the given traceset 19 | */ 20 | async createRelevantLogStatuses( 21 | traceLog: TraceLog, 22 | manager: EntityManager | null = null 23 | ): Promise { 24 | if (traceLog.id == null) { 25 | throw new PublicError("trace log unitialized"); 26 | } 27 | if (manager == null) { 28 | manager = getManager(); 29 | } 30 | const probeRepository = manager.getCustomRepository(ProbeRepository); 31 | const relevantProbeIds = await probeRepository.findActiveProbesIds( 32 | traceLog.traceSetId 33 | ); 34 | 35 | const newManager: EntityManager = manager; 36 | // manager has to be reassigned to ensure 37 | // that it stays not tull in the next line 38 | return relevantProbeIds.map((id) => 39 | newManager.create( 40 | TraceLogStatus, 41 | TraceLogStatus.newTraceLogstatus({ 42 | probeId: id, 43 | traceLogId: traceLog.id, 44 | }) 45 | ) 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /backend/src/resolvers/__tests__/dependency_injection_test.ts: -------------------------------------------------------------------------------- 1 | import { Service, Container, Token } from "typedi"; 2 | 3 | beforeAll(() => { 4 | Container.reset(); 5 | }); 6 | 7 | afterAll(() => { 8 | Container.reset(); 9 | }); 10 | 11 | test("whether or not multiple service declaration allows for the service to be injected", async () => { 12 | interface Test { 13 | info: string; 14 | } 15 | const token = new Token("tests"); 16 | 17 | @Service({ id: token, multiple: true }) 18 | class Test1 implements Test { 19 | info: "test1"; 20 | } 21 | 22 | @Service({ id: token, multiple: true }) 23 | class Test2 implements Test { 24 | info: "tests2"; 25 | } 26 | 27 | const tests = Container.getMany(token); 28 | const test1 = Container.get(Test1); 29 | const test2 = Container.get(Test2); 30 | expect(tests[0].info).toStrictEqual(test1.info); 31 | expect(tests[1].info).toStrictEqual(test2.info); 32 | expect(tests[0]).toStrictEqual(test1); 33 | expect(tests[1]).toStrictEqual(test2); 34 | }); 35 | -------------------------------------------------------------------------------- /backend/src/resolvers/file_resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | FieldResolver, 4 | Root, 5 | Arg, 6 | Query, 7 | Mutation, 8 | Ctx, 9 | } from "type-graphql"; 10 | import { Inject } from "typedi"; 11 | import { StorageService, streamToString } from "../services/storage"; 12 | 13 | import { FileInfo, FunctionInfo, ClassInfo } from "../entities"; 14 | import { EntityManager } from "typeorm"; 15 | import { InjectManager } from "typeorm-typedi-extensions"; 16 | import { FileInfoRepository } from "../repositories/file_info_repository"; 17 | import { Context } from "../context"; 18 | import { DirectoryInfoRepository } from "../repositories/directory_info_repository"; 19 | 20 | @Resolver((of) => FileInfo) 21 | export class FileResolver { 22 | private readonly fileInfoRepository: FileInfoRepository; 23 | private readonly directoryInfoRepository: DirectoryInfoRepository; 24 | constructor( 25 | @InjectManager() 26 | private readonly manager: EntityManager, 27 | @Inject((type) => StorageService) 28 | private readonly storageService: StorageService 29 | ) { 30 | this.fileInfoRepository = manager.getCustomRepository( 31 | FileInfoRepository 32 | ); 33 | this.directoryInfoRepository = manager.getCustomRepository( 34 | DirectoryInfoRepository 35 | ); 36 | } 37 | 38 | @Query((type) => FileInfo, { nullable: true }) 39 | async file( 40 | /* eslint-disable */ 41 | @Arg("fileId", (type) => String, { nullable: true }) 42 | fileId: string 43 | /* eslint-enable */ 44 | ): Promise { 45 | return await this.manager.findOne(FileInfo, fileId); 46 | } 47 | 48 | @FieldResolver((type) => String, { nullable: false }) 49 | async content(@Root() file: FileInfo): Promise { 50 | return await streamToString( 51 | await this.storageService.load(file.objectName) 52 | ); 53 | } 54 | 55 | @FieldResolver((type) => [FunctionInfo], { nullable: false }) 56 | async functions(@Root() file: FileInfo): Promise { 57 | return this.fileInfoRepository.functions(file); 58 | } 59 | 60 | @FieldResolver((type) => [ClassInfo], { nullable: false }) 61 | async classes(@Root() file: FileInfo): Promise { 62 | return this.fileInfoRepository.classes(file); 63 | } 64 | 65 | @Mutation((type) => Boolean, { 66 | nullable: false, 67 | description: 68 | "returns true if the root directory was removed false if it didn't exist in the first place", 69 | }) 70 | async removeRootDirectory(@Ctx() context: Context): Promise { 71 | const traceSetId = (await context.traceSet()).id; 72 | return !!(await this.directoryInfoRepository.genRemoveRootDir( 73 | traceSetId 74 | )); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /backend/src/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | // AUTOGENERATED FROM scripts/make_resolvers.py DO NOT MODIFY 2 | import { CodeResolver } from "./code_resolver"; 3 | import { FileResolver } from "./file_resolver"; 4 | import { LiveTailResolver } from "./live_tail_resolver"; 5 | import { ProbeFailureResolver } from "./probe_failure_resolver"; 6 | import { ProbeResolver } from "./probe_resolver"; 7 | import { TraceResolver } from "./trace_resolver"; 8 | import { TraceSetResolver } from "./trace_set_resolver"; 9 | import { UserResolver } from "./user_resolver"; 10 | 11 | export const ALL_RESOLVERS = [ 12 | CodeResolver, 13 | FileResolver, 14 | LiveTailResolver, 15 | ProbeFailureResolver, 16 | ProbeResolver, 17 | TraceResolver, 18 | TraceSetResolver, 19 | UserResolver, 20 | ]; 21 | -------------------------------------------------------------------------------- /backend/src/resolvers/live_tail_resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Root, 4 | Arg, 5 | Mutation, 6 | Subscription, 7 | PubSub, 8 | PubSubEngine, 9 | Ctx, 10 | } from "type-graphql"; 11 | import { EntityManager } from "typeorm"; 12 | import { InjectManager } from "typeorm-typedi-extensions"; 13 | import { genLogTopic } from "../topics"; 14 | import { Context } from "../context"; 15 | 16 | @Resolver() 17 | export class LiveTailResolver { 18 | constructor( 19 | @InjectManager() 20 | private readonly manager: EntityManager 21 | ) {} 22 | 23 | @Mutation((type) => [String], { nullable: false }) 24 | async publishLog( 25 | @Arg("content", (type) => [String]) content: string[], 26 | @Ctx() context: Context, 27 | @PubSub() pubsub: PubSubEngine 28 | ): Promise { 29 | const tail = await context.probe.traceSet; 30 | await pubsub.publish(genLogTopic(tail.id), content); 31 | return content; 32 | } 33 | 34 | @Subscription((type) => [String], { 35 | nullable: false, 36 | topics: ({ args }) => genLogTopic(args.traceSetId), 37 | }) 38 | listenLog( 39 | @Arg("traceSetId") traceSetId: string, 40 | @Root() content: string[] 41 | ): string[] { 42 | return content; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/resolvers/probe_failure_resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Field, InputType, Arg, Ctx, Mutation } from "type-graphql"; 2 | import { EntityManager } from "typeorm"; 3 | import { InjectManager } from "typeorm-typedi-extensions"; 4 | 5 | import { ProbeFailure, Trace } from "../entities"; 6 | import { Context } from "../context"; 7 | import { createTransaction, PublicError } from "../utils"; 8 | import { ProbeFailureRepository } from "../repositories/probe_failure_repository"; 9 | 10 | @InputType() 11 | class NewProbeFailureInput { 12 | @Field({ nullable: true }) 13 | traceId?: string; 14 | @Field({ nullable: false }) 15 | message: string; 16 | } 17 | 18 | @Resolver((of) => ProbeFailure) 19 | export class ProbeFailureResolver { 20 | constructor( 21 | @InjectManager() 22 | private readonly manager: EntityManager 23 | ) {} 24 | 25 | /** 26 | * createFailure creates a failure instance 27 | * NOTE: THIS DOES NOT SAVE THE INSTANCE TO THE DB 28 | */ 29 | private async createFailure( 30 | { traceId, message }: NewProbeFailureInput, 31 | context: Context, 32 | manager: EntityManager 33 | ) { 34 | const probe = context.probe; 35 | const failure = manager.create(ProbeFailure, { 36 | message, 37 | probeId: probe.id, 38 | }); 39 | 40 | if (traceId) { 41 | const trace = await manager.getRepository(Trace).findOne({ 42 | where: { id: traceId }, 43 | }); 44 | if (trace == null) { 45 | throw new PublicError("could not find trace with that id"); 46 | } 47 | failure.traceId = traceId; 48 | failure.traceVersion = trace.version; 49 | } 50 | return failure; 51 | } 52 | 53 | @Mutation((returns) => ProbeFailure) 54 | async newProbeFailure( 55 | @Arg("newProbeFailure") input: NewProbeFailureInput, 56 | @Ctx() context: Context 57 | ): Promise { 58 | return await createTransaction(this.manager, async (manager) => { 59 | const probeFailureRepository = manager.getCustomRepository( 60 | ProbeFailureRepository 61 | ); 62 | 63 | const probe = context.probe; 64 | const failure = await this.createFailure(input, context, manager); 65 | 66 | const existingFailure = await probeFailureRepository.findExistingFailure( 67 | failure, 68 | probe 69 | ); 70 | 71 | if (!existingFailure) return await manager.save(failure); 72 | return existingFailure; 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /backend/src/resolvers/user_resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query, Ctx } from "type-graphql"; 2 | import { EntityManager } from "typeorm"; 3 | import { InjectManager } from "typeorm-typedi-extensions"; 4 | import { User } from "../entities"; 5 | import { Context } from "../context"; 6 | 7 | @Resolver((of) => User) 8 | export class UserResolver { 9 | constructor( 10 | @InjectManager() 11 | private readonly manager: EntityManager 12 | ) {} 13 | 14 | @Query((returns) => User, { nullable: true }) 15 | async me(@Ctx() { user }: Context): Promise { 16 | return user; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/services/storage.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "typedi"; 2 | import { Client } from "minio"; 3 | import { config } from "./../config"; 4 | import { Stream } from "stream"; 5 | import { logger } from "../logging"; 6 | import { Logger } from "winston"; 7 | 8 | /** 9 | * converts strams to buffers 10 | */ 11 | export const streamToBuffer = (stream: Stream) => { 12 | const chunks: Uint8Array[] = []; 13 | return new Promise((resolve, reject) => { 14 | stream.on("data", (chunk: Uint8Array) => chunks.push(chunk)); 15 | stream.on("error", (_) => reject(new Error("stream parse error"))); 16 | stream.on("end", () => resolve(Buffer.concat(chunks))); 17 | }); 18 | }; 19 | 20 | /** 21 | * converts the stream to a string 22 | */ 23 | export const streamToString = async (stream: Stream) => { 24 | return (await streamToBuffer(stream)).toString("utf-8"); 25 | }; 26 | 27 | @Service() 28 | export class StorageService { 29 | private client: Client; 30 | private started: boolean; 31 | private logger: Logger; 32 | 33 | async start() { 34 | if (!this.started) { 35 | this.started = true; 36 | } 37 | if (!(await this.client.bucketExists(config.storage.bucket))) { 38 | await this.client.makeBucket( 39 | config.storage.bucket, 40 | config.storage.region 41 | ); 42 | } 43 | } 44 | 45 | async load(name: string) { 46 | await this.start(); 47 | return await this.client.getObject(config.storage.bucket, name); 48 | } 49 | 50 | async save(name: string, data: Buffer) { 51 | await this.start(); 52 | return await this.client.putObject(config.storage.bucket, name, data); 53 | } 54 | 55 | async remove(name: string) { 56 | await this.start(); 57 | return await this.client.removeObject(config.storage.bucket, name); 58 | } 59 | 60 | constructor() { 61 | this.client = new Client({ 62 | ...config.storage.client, 63 | }); 64 | this.started = false; 65 | this.logger = logger.child({ service: "storage" }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /backend/src/topics.ts: -------------------------------------------------------------------------------- 1 | export function genProbeTopic(traceSetId: string): string { 2 | return `PROBE:${traceSetId}`; 3 | } 4 | 5 | export function genLogTopic(traceSetId: string): string { 6 | return `LOG:${traceSetId}`; 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/utils/__tests__/transaction_test.ts: -------------------------------------------------------------------------------- 1 | import { DebugConnector } from "../../connect"; 2 | import { Container } from "typedi"; 3 | import { EntityManager, getManager } from "typeorm"; 4 | import { UploadService } from "../../services/upload"; 5 | import { FileInfo, TraceSet } from "../../entities"; 6 | import { plainToClass } from "class-transformer"; 7 | import { StorageService } from "../../services/storage"; 8 | import { DirectoryInfoRepository } from "../../repositories/directory_info_repository"; 9 | import { seedTriple } from "../../helpers"; 10 | import { createTransaction } from ".."; 11 | 12 | describe("setting up dummy file", () => { 13 | let manager: EntityManager; 14 | beforeAll(async () => { 15 | await new DebugConnector().connect(); 16 | manager = getManager(); 17 | }); 18 | 19 | it("test if transaction is active in nested transaction", async () => { 20 | await expect( 21 | createTransaction( 22 | manager, 23 | async (manager) => 24 | manager.queryRunner && 25 | manager.queryRunner.isTransactionActive 26 | ) 27 | ).resolves.toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /backend/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { EntityManager } from "typeorm"; 2 | 3 | export class PublicError extends Error {} 4 | 5 | export function assertNotNull(value: T | null | undefined): T { 6 | if (value === null || value === undefined) { 7 | throw new Error("value was null"); 8 | } 9 | return value; 10 | } 11 | 12 | /** 13 | * small wrapper over manager.transaction 14 | * so that that nested transactions are are flattened 15 | */ 16 | export function createTransaction( 17 | manager: EntityManager, 18 | transaction: (manager: EntityManager) => Promise 19 | ): Promise { 20 | if (manager.queryRunner && manager.queryRunner.isTransactionActive) { 21 | return transaction(manager); 22 | } 23 | return manager.transaction(transaction); 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/utils/testing.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFormattedError, DocumentNode } from "graphql"; 2 | import { 3 | ApolloServerTestClient, 4 | createTestClient, 5 | } from "apollo-server-testing"; 6 | import { ApolloServer } from "apollo-server"; 7 | 8 | export type GQLResponse = { 9 | data?: D; 10 | errors?: ReadonlyArray; 11 | }; 12 | 13 | declare type Query = { 14 | query: DocumentNode | string; 15 | variables: VariableType; 16 | operationName?: string; 17 | }; 18 | declare type Mutation = { 19 | mutation: DocumentNode | string; 20 | variables: VariableType; 21 | operationName?: string; 22 | }; 23 | 24 | export class TestClientWrapper { 25 | constructor(private client: ApolloServerTestClient) {} 26 | 27 | async query(query: Query): Promise> { 28 | return (await this.client.query(query)) as GQLResponse; 29 | } 30 | 31 | async mutate( 32 | mutation: Mutation 33 | ): Promise> { 34 | return (await this.client.mutate(mutation)) as GQLResponse; 35 | } 36 | } 37 | 38 | export function createWrappedTestClient( 39 | server: ApolloServer 40 | ): TestClientWrapper { 41 | return new TestClientWrapper(createTestClient(server)); 42 | } 43 | -------------------------------------------------------------------------------- /backend/src/version.ts: -------------------------------------------------------------------------------- 1 | export const version = "0.4.4"; 2 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "esModuleInterop": true, 5 | "experimentalDecorators": true, 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "incremental": true, 10 | "outDir": "./build", 11 | "sourceMap": true, 12 | "lib": ["esnext"], 13 | "target": "es6", 14 | "strictNullChecks": true 15 | }, 16 | "include": ["src/**/*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /docker-compose.env: -------------------------------------------------------------------------------- 1 | # minio 2 | MINIO_ACCESS_KEY=minio 3 | MINIO_SECRET_KEY=minio123 4 | MINIO_HOST=minio 5 | MINIO_PORT=9000 6 | 7 | # backend 8 | BACKEND_HOST=backend 9 | BACKEND_PORT=4000 10 | BACKEND_SYNCHRONIZE= 11 | 12 | # frontend 13 | FRONTEND_HOST=localhost 14 | FRONTEND_PORT=3000 15 | 16 | # database 17 | POSTGRES_HOST=postgres 18 | POSTGRES_PORT=5432 19 | ## if you use this in production make sure to change these values 20 | POSTGRES_USER=postgres 21 | POSTGRES_PASSWORD=postgres 22 | POSTGRES_DB=postgres 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | backend: 5 | depends_on: 6 | - minio 7 | - postgres 8 | build: 9 | context: ./backend 10 | dockerfile: ./docker/prod.Dockerfile 11 | ports: 12 | - "4000:4000" 13 | env_file: 14 | - docker-compose.env 15 | 16 | frontend: 17 | depends_on: 18 | - backend 19 | build: 20 | context: ./frontend 21 | dockerfile: ./docker/dev.Dockerfile 22 | ports: 23 | - "3000:3000" 24 | env_file: 25 | - docker-compose.env 26 | 27 | docs: 28 | build: 29 | context: ./docs 30 | dockerfile: ./docker/dev.Dockerfile 31 | ports: 32 | - "3001:3001" 33 | env_file: 34 | - docker-compose.env 35 | 36 | # backend dependencies 37 | minio: 38 | image: minio/minio:RELEASE.2020-04-10T03-34-42Z 39 | volumes: 40 | - data:/data 41 | ports: 42 | - "9000:9000" 43 | command: server /data 44 | healthcheck: 45 | test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] 46 | interval: 30s 47 | timeout: 20s 48 | retries: 3 49 | env_file: 50 | - docker-compose.env 51 | 52 | postgres: 53 | image: postgres:12 54 | volumes: 55 | - pgdata:/var/lib/postgres/data 56 | ports: 57 | - "5432:5432" 58 | env_file: 59 | - docker-compose.env 60 | 61 | volumes: 62 | data: 63 | pgdata: 64 | -------------------------------------------------------------------------------- /docs/.dockerignore: -------------------------------------------------------------------------------- 1 | .gitignore -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # The binary to build (just the basename). 2 | MODULE := docs 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 | run: 16 | yarn run start 17 | 18 | build: 19 | yarn run build 20 | 21 | 22 | # Example: make build-prod VERSION=1.0.0 23 | build-prod: 24 | @echo "\n${BLUE}Building Production image with labels:\n" 25 | @echo "name: $(MODULE)" 26 | @echo "version: $(VERSION)${NC}\n" 27 | @sed \ 28 | -e 's|{NAME}|$(MODULE)|g' \ 29 | -e 's|{VERSION}|$(VERSION)|g' \ 30 | docker/prod.Dockerfile | docker build -t $(IMAGE):$(VERSION) -f- . 31 | 32 | 33 | build-dev: 34 | @echo "\n${BLUE}Building Development image with labels:\n" 35 | @echo "name: $(MODULE)" 36 | @echo "version: $(TAG)${NC}\n" 37 | @sed \ 38 | -e 's|{NAME}|$(MODULE)|g' \ 39 | -e 's|{VERSION}|$(TAG)|g' \ 40 | docker/dev.Dockerfile | docker build -t $(IMAGE):$(TAG) -f- . 41 | 42 | # Example: make push VERSION=0.0.2 43 | push: build-prod 44 | @echo "\n${BLUE}Pushing image to GitHub Docker Registry...${NC}\n" 45 | @docker push $(IMAGE):$(VERSION) 46 | 47 | push-latest: push 48 | docker tag $(IMAGE):$(VERSION) $(IMAGE):latest 49 | @docker push $(IMAGE):latest 50 | 51 | version: 52 | @echo $(TAG) 53 | 54 | .PHONY: clean image-clean build-prod push test build 55 | 56 | clean: 57 | rm -rf node_modules/ 58 | 59 | docker-clean: 60 | @docker system prune -f --filter "label=name=$(MODULE)" 61 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | ``` 30 | $ GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docker/dev.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.12-buster-slim AS builder 2 | WORKDIR /app 3 | COPY package.json package.json 4 | COPY yarn.lock yarn.lock 5 | RUN yarn install 6 | 7 | FROM builder AS runner 8 | COPY . . 9 | 10 | CMD yarn run start 11 | 12 | LABEL name={NAME} 13 | LABEL version={VERSION} 14 | -------------------------------------------------------------------------------- /docs/docker/prod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.12-buster-slim AS builder 2 | WORKDIR /app 3 | COPY package.json package.json 4 | COPY yarn.lock yarn.lock 5 | RUN yarn install 6 | 7 | FROM builder AS runner 8 | COPY . . 9 | RUN yarn build 10 | 11 | FROM nginx:1.19 AS deploy 12 | COPY --from=runner /app/build /usr/share/nginx/html 13 | 14 | LABEL name={NAME} 15 | LABEL version={VERSION} 16 | -------------------------------------------------------------------------------- /docs/docs/examples/mdx.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: mdx 3 | title: Powered by MDX 4 | --- 5 | 6 | You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/). 7 | 8 | export const Highlight = ({children, color}) => ( {children} ); 14 | 15 | Docusaurus green and Facebook blue are my favorite colors. 16 | 17 | I can write **Markdown** alongside my _JSX_! 18 | -------------------------------------------------------------------------------- /docs/docs/getting_started.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: getting_started 3 | title: Get Started 4 | --- 5 | 6 | Getting up inquest just takes 2 steps! 7 | :::note 8 | Inquest is only verified to work with python versions 3.7 and later 9 | ::: 10 | 11 | ## 1. Install It With `pip` 12 | 13 | Just run 14 | 15 | ```python 16 | pip install inquest 17 | ``` 18 | 19 | Make sure to add it to your requirements.txt. 20 | 21 | ## 2. Import It Into Your Code 22 | 23 | 24 | initialize it once at the start of your python script. 25 | For a normal python script that just means putting it before the 26 | first line that's run. 27 | 28 | ```python 29 | 30 | import inquest 31 | 32 | def main(): 33 | inquest.enable(api_key='', glob=["main.py", "some_subdirectory/**/*.py"]) 34 | ... 35 | 36 | if __name__ == '__main__': 37 | main() 38 | ``` 39 | 40 | Retrieve your `api_key` from the sidebar after you log into in the dashboard. Put the files you want 41 | to have access to in dashboard into the `glob` parameter. 42 | 43 | ### More Info On Globbing Files 44 | 45 | To upload all python files are in the directory `my_directory`, the glob `my_directory/**/*.py` will 46 | verify and upload all these files. The files will be uploaded securely to backend so you can view 47 | it in the dashboard. Once you've uploaded them, you can delete them any time. 48 | 49 | Glob takes in any list of valid python globs. So for more complex use cases, read python's standard 50 | library on [globs](https://docs.python.org/3/library/glob.html). 51 | 52 | We check against the files' hashes so files are only uploaded if they've been modified between runs. 53 | 54 | ## What's Next? 55 | 56 | You're set up! Now just run your python script like you usually do and go on [the dashboard](https://inquest.dev/dashboard) to get started. 57 | -------------------------------------------------------------------------------- /docs/docs/getting_started_with_docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: getting_started_with_docker 3 | title: Self-Hosting With Docker 4 | --- 5 | 6 | If you want to run Inquest on your own computer, it takes just three 7 | steps. 8 | 9 | :::note 10 | Inquest is only verified to work with python versions 3.7 and later 11 | 12 | If you just want to try it out quickly with less installation 13 | [follow this guide](getting_started.md) on how to set it up with Inquest Cloud. It's also free, but it takes less work to set up. 14 | ::: 15 | 16 | ## 1. Setup Up The Inquest API Server & Frontend 17 | 18 | First, clone our repo 19 | 20 | ```bash 21 | git clone https://github.com/yiblet/inquest.git 22 | ``` 23 | 24 | Then, once inside the directory, you simply have to run 25 | 26 | ```bash 27 | docker-compose up 28 | ``` 29 | 30 | to get services running. By default the frontend dashboard will be located at `localhost:3000` and 31 | the api backend will be at `localhost:4000`. 32 | 33 | :::note 34 | If you haven't installed docker, [Here's a link](https://docs.docker.com/get-docker/) to get started, and 35 | to install docker-compose simply run `pip install docker-compose` 36 | ::: 37 | 38 | ## 2. Install It With `pip` 39 | 40 | Just run 41 | 42 | ```python 43 | pip install inquest 44 | ``` 45 | 46 | Make sure to add it to your requirements.txt. 47 | 48 | ## 3. Import It Into Your Code 49 | 50 | initialize it once at the start of your python script. 51 | For a normal python script that just means putting it before the 52 | first line that's run. 53 | 54 | ```python 55 | 56 | import inquest 57 | 58 | def main(): 59 | inquest.enable(api_key='', glob=["main.py", "some_subdirectory/**/*.py"]) 60 | ... 61 | 62 | if __name__ == '__main__': 63 | main() 64 | ``` 65 | 66 | Retrieve your `api_key` from the sidebar after you log into in the dashboard. Put the files you want 67 | to have access to in dashboard into the `glob` parameter. 68 | 69 | 70 | ### More Info On Globbing Files 71 | 72 | To upload all python files are in the directory `my_directory`, the glob `my_directory/**/*.py` will 73 | verify and upload all these files. The files will be uploaded securely to backend so you can view 74 | it in the dashboard. Once you've uploaded them, you can delete them any time. 75 | 76 | Glob takes in any list of valid python globs. So for more complex use cases, read python's standard 77 | library on [globs](https://docs.python.org/3/library/glob.html). 78 | 79 | We check against the files' hashes so files are only uploaded if they've been modified between runs. 80 | 81 | ## What's Next? 82 | 83 | You're set up! Now just run your python script like you usually do, and go to `localhost:3000` on your 84 | browser to get access to your own personal copy of Inquest. 85 | -------------------------------------------------------------------------------- /docs/docs/logs_dont_appear.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: logs_dont_appear 3 | title: My Logs Don't Appear 4 | --- 5 | 6 | There are two main cases for why this is happening: 7 | 8 | 1. The Inquest Probe (your python instance) is disconnected from the backend 9 | 2. You're running your code in an infinite loop 10 | 3. You Have A An Existing Yellow Warning 11 | 12 | ## You're disconnected 13 | 14 | Check the dashboard's sidebar to see if you're disconnected. If you see "0 instances connected" that means 15 | that your python instance was disconnected. Inquest by default outputs some kind of error message to stdout 16 | when it unexpectedly stops working so take a look at that. 17 | 18 | ## You're running your code an infinite loop 19 | 20 | When you add a new log to some function, for safety reasons, Inquest doesn't modify the function but modifies the pointer to the function to point to the the updated version of the function. This means that the function has to be called from a different function for it to run the modified version. In an infinite loop, the function is never called from a different function. 21 | 22 | To avoid this issue, we reccomend breaking up the part you want to log with inquest into a different function. So that the infinite loop part of the code calls into the part of the code that houses the lines you want to log. 23 | 24 | 25 | ## You have an existing yellow warning 26 | 27 | Inquest tries to be as safe as possible when modifying running code, and that means stopping early when it 28 | sees a yellow warning. You must address all yellow warnings before adding new logs. Even the warnings 29 | in different files. Or else your new changes won't take affect on the python side. 30 | 31 | [More on yellow warnings here](./yellow_warning.md) 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/docs/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: overview 3 | title: Overview 4 | --- 5 | 6 | Welcome to the Inquest documentation. Inquest is a real-time logging framework for python. It let's you add 7 | log statements and other metrics to running code in the same way you add 8 | break points in a debugger. 9 | 10 | After two lines of integration in python, you're able to to add a log statement and in milliseconds that new log statement is inserted in to your running python without requiring any restarts. 11 | 12 | To get started, there are 2 ways of running inquest: 13 | 14 | 1. [Running It With Inquest Cloud](getting_started.md) 15 | 2. [Hosting it on a your computer via docker-compose](getting_started_with_docker.md) 16 | 17 | Right now, during beta, Inquest Cloud is completely free (and we'll permanently have a free-tier). If you want to just try it out without hosting, you can get started for free. 18 | -------------------------------------------------------------------------------- /docs/docs/running_inquest.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: running_inquest 3 | title: inquest.enable 4 | --- 5 | 6 | ```python 7 | inquest.enable( 8 | api_key, 9 | host= "inquest.dev", 10 | port= 443, 11 | glob= None, 12 | exclude = None, 13 | daemon = True, 14 | ) 15 | ``` 16 | 17 | ### Examples 18 | 19 | In general, you only need to pass inquest 2 arguments. However, if you're self-hosting 20 | you'll have to pass in a two more. 21 | 22 | for connecting to Inquest cloud: 23 | 24 | ```python 25 | inquest.enable( 26 | api_key='random-key-231312', 27 | glob= "my-project/**/*.py", 28 | ) 29 | ``` 30 | 31 | For connecting your own self-hosted Inquest backend (which is at `localhost:3000` in this example): 32 | 33 | ```python 34 | inquest.enable( 35 | api_key='random-key-231312', 36 | host='localhost', 37 | port=3000, 38 | glob= "my_project/**/*.py", 39 | ) 40 | ``` 41 | 42 | ### Arguments For `inquest.enable` 43 | 44 | | argument | description | 45 | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 46 | | `api_key` | Your api key (you can get this by logging into the inquest dashboard). | 47 | | `glob` | a list of [recursive globs](https://docs.python.org/3/library/glob.html) pointing to files that will be uploaded to the dashboard, so you can add log statements to them. We check the files' hashes so files are only uploaded if they've been changed since last run or if they've never been uploaded before. | 48 | | \*`host` | pass in the url of your inquest backend. | 49 | | \*`port` | the port of your inquest backend. | 50 | | \*`ssl` | if the port is set to 443 this is automatically set to true. Otherwise it's set to false. Set to true only if your self-hosted backend is set up with ssl. | 51 | 52 | \*only needed if you're self-hosting. 53 | -------------------------------------------------------------------------------- /docs/docs/yellow_warning.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: yellow_warning 3 | title: I Have A Yellow Warning 4 | --- 5 | 6 | The yellow warning in the dashboard signifies some issue along the steps towards installing the new 7 | log. Most commonly there are 3 issues that cause this: 8 | 9 | 10 | 1. The log is throwing an exception 11 | 2. The file is not imported 12 | 3. You're using unwrapped python decorators. 13 | 14 | ## The log is throwing an exception 15 | 16 | What's inside the `{bracket}` notation in your logs are full valid python expressions that can throw 17 | errors. Often these errors are something as benign as a misspelled variable, but it's possible to 18 | throw more complicated errors. 19 | 20 | To fix this look more carefully into what exactly you're logging. 21 | 22 | ## The file is not imported 23 | 24 | If you're trying to add a log statement to a python function that is never imported, inquest will 25 | attempt to find that function, fail, and then report it as a yellow warning. 26 | 27 | ## You're using unwrapped decorators 28 | 29 | When we're modifying a python function that's behind a decorator we use the decorator's metadate 30 | to find the original function. Specifically through the `__wrapped__` attribute. 31 | 32 | This error should only be a concern if you wrote the decorator yourself, most popular python libraries 33 | implement decorators with wraps. 34 | 35 | The `__wrapped__` attribute doesn't get set automatically. Instead, to add the attribute you need 36 | to use a function from the python standard library. 37 | 38 | [More on that in the python standard library docs](https://docs.python.org/3.8/library/functools.html#functools.wraps) 39 | 40 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "docusaurus start -p 3001 -h 0.0.0.0", 7 | "build": "docusaurus build", 8 | "swizzle": "docusaurus swizzle", 9 | "deploy": "docusaurus deploy" 10 | }, 11 | "dependencies": { 12 | "@docusaurus/core": "^2.0.0-alpha.58", 13 | "@docusaurus/preset-classic": "^2.0.0-alpha.58", 14 | "clsx": "^1.1.1", 15 | "react": "^16.8.4", 16 | "react-dom": "^16.8.4", 17 | "react-typist": "^2.0.5" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | docs: [ 3 | { 4 | type: "doc", 5 | id: "overview" 6 | }, 7 | { 8 | "Quick Start": ["getting_started", "getting_started_with_docker"], 9 | Library: ["running_inquest"], 10 | "Troubleshooting": ["logs_dont_appear", "yellow_warning"] 11 | } 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | @import url("https://fonts.googleapis.com/css2?family=Lato&family=Roboto:wght@400;500;700&display=swap"); 9 | 10 | /* You can override the default Infima variables here. */ 11 | :root { 12 | --ifm-color-primary: #4299e1; 13 | --ifm-color-primary-dark: #3182ce; 14 | --ifm-color-primary-darker: #2b6cb0; 15 | --ifm-color-primary-darkest: #2c5282; 16 | --ifm-color-primary-light: #63b3ed; 17 | --ifm-color-primary-lighter: #90cdf4; 18 | --ifm-color-primary-lightest: #bee3f8; 19 | --ifm-code-font-size: 80%; 20 | --ifm-font-family-base: Roboto, system-ui, -apple-system, BlinkMacSystemFont, 21 | "Segoe UI", "Helvetica Neue", Arial, "Noto Sans", sans-serif, 22 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 23 | } 24 | 25 | .navbar__title { 26 | font-family: Lato, Roboto, system-ui, -apple-system, BlinkMacSystemFont, 27 | "Segoe UI", "Helvetica Neue", Arial, "Noto Sans", sans-serif, 28 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 29 | letter-spacing: 0.9px; 30 | font-weight: 400; 31 | text-transform: uppercase; 32 | font-size: 120%; 33 | } 34 | 35 | .docusaurus-highlight-code-line { 36 | background-color: rgb(72, 77, 91); 37 | display: block; 38 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 39 | padding: 0 var(--ifm-pre-padding); 40 | } 41 | -------------------------------------------------------------------------------- /docs/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 4rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | .heroContent { 16 | margin: 0 auto; 17 | max-width: 40rem; 18 | text-align: center; 19 | color: white; 20 | } 21 | 22 | @media screen and (max-width: 966px) { 23 | .heroBanner { 24 | padding: 2rem; 25 | } 26 | } 27 | 28 | .button { 29 | color: white; 30 | font-size: 1.5rem; 31 | font-weight: 700; 32 | text-transform: uppercase; 33 | border-color: white; 34 | padding: 0.5rem 1.5rem; 35 | border: 2px solid; 36 | border-radius: 10px; 37 | transition: all 0.4s; 38 | } 39 | 40 | .button:hover { 41 | background-color: white; 42 | color: var(--ifm-color-primary); 43 | text-decoration: none; 44 | } 45 | 46 | .buttons { 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | } 51 | 52 | .features { 53 | display: flex; 54 | align-items: center; 55 | padding: 2rem 0; 56 | width: 100%; 57 | } 58 | 59 | .featureImage { 60 | height: 200px; 61 | width: 200px; 62 | } 63 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/docs/static/.nojekyll -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | docker/ 4 | .dockerignore 5 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ENDPOINT=inquest.dev 2 | NEXT_PUBLIC_DOCS_ENDPOINT=docs.inquest.dev 3 | -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ENDPOINT=localhost:4000 2 | NEXT_PUBLIC_DOCS_ENDPOINT=localhost:3001 3 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es6: true, 5 | }, 6 | settings: { 7 | react: { 8 | version: "detect", // React version. "detect" automatically picks the version you have installed. 9 | }, 10 | }, 11 | parser: "@typescript-eslint/parser", 12 | parserOptions: { 13 | ecmaFeatures: { 14 | experimentalObjectRestSpread: true, 15 | jsx: true, 16 | }, 17 | sourceType: "module", 18 | }, 19 | plugins: ["@typescript-eslint"], 20 | extends: [ 21 | "eslint:recommended", 22 | "plugin:@typescript-eslint/eslint-recommended", 23 | "plugin:@typescript-eslint/recommended", 24 | "plugin:react/recommended", 25 | ], 26 | rules: { 27 | indent: [1, 4], 28 | "linebreak-style": [2, "unix"], 29 | quotes: [2, "double"], 30 | "@typescript-eslint/no-unused-vars": [1, { args: "none" }], 31 | "@typescript-eslint/explicit-function-return-type": "off", 32 | "@typescript-eslint/no-explicit-any": "off", 33 | "@typescript-eslint/ban-ts-ignore": "off", 34 | "react/prop-types": "off", 35 | "react/no-unescaped-entities": "off", 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # parcel-bundler cache (https://parceljs.org/) 67 | .cache 68 | 69 | # next.js build output 70 | .next 71 | 72 | # nuxt.js build output 73 | .nuxt 74 | 75 | # vuepress build output 76 | .vuepress/dist 77 | 78 | # Serverless directories 79 | .serverless/ 80 | 81 | # FuseBox cache 82 | .fusebox/ 83 | 84 | # DynamoDB Local files 85 | .dynamodb/ 86 | 87 | # End of https://www.gitignore.io/api/node 88 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | dist 6 | src/generated 7 | src/**/*.snap 8 | -------------------------------------------------------------------------------- /frontend/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from "@storybook/react"; 2 | // automatically import all files ending in *.stories.tsx 3 | configure(require.context("../src", true, /\.stories\.tsx?$/), module); 4 | -------------------------------------------------------------------------------- /frontend/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = ({ config }) => { 4 | config.module.rules.push({ 5 | test: /\.(ts|tsx)$/, 6 | loader: require.resolve("babel-loader"), 7 | options: { 8 | presets: [require.resolve("babel-preset-react-app")], 9 | }, 10 | }); 11 | 12 | config.module.rules.push({ 13 | test: /\.css$/, 14 | use: [ 15 | { 16 | loader: "postcss-loader", 17 | options: { 18 | ident: "postcss", 19 | config: { 20 | path: "./.storybook", 21 | }, 22 | plugins: [ 23 | require("postcss-import"), 24 | require("tailwindcss"), 25 | require("autoprefixer"), 26 | ], 27 | }, 28 | }, 29 | ], 30 | include: path.resolve(__dirname, "../"), 31 | }); 32 | 33 | config.resolve.extensions.push(".ts", ".tsx"); 34 | return config; 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/Makefile: -------------------------------------------------------------------------------- 1 | # The binary to build (just the basename). 2 | MODULE := frontend 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 | run: 16 | yarn run dev 17 | 18 | test: 19 | @yarn test 20 | 21 | build: 22 | yarn run tsc 23 | 24 | lint: 25 | @yarn lint 26 | 27 | 28 | fix: 29 | @yarn fix 30 | 31 | 32 | # Example: make build-prod VERSION=1.0.0 33 | build-prod: 34 | @echo "\n${BLUE}Building Production image with labels:\n" 35 | @echo "name: $(MODULE)" 36 | @echo "version: $(VERSION)${NC}\n" 37 | @sed \ 38 | -e 's|{NAME}|$(MODULE)|g' \ 39 | -e 's|{VERSION}|$(VERSION)|g' \ 40 | docker/prod.Dockerfile | docker build -t $(IMAGE):$(VERSION) -f- . 41 | 42 | 43 | build-dev: 44 | @echo "\n${BLUE}Building Development image with labels:\n" 45 | @echo "name: $(MODULE)" 46 | @echo "version: $(TAG)${NC}\n" 47 | @sed \ 48 | -e 's|{NAME}|$(MODULE)|g' \ 49 | -e 's|{VERSION}|$(TAG)|g' \ 50 | docker/dev.Dockerfile | docker build -t $(IMAGE):$(TAG) -f- . 51 | 52 | set-version: 53 | # sed 's/"version".*/"version": "$(VERSION)",/g' -i package.json 54 | 55 | # Example: make shell CMD="-c 'date > datefile'" 56 | shell: build-dev 57 | @echo "\n${BLUE}Launching a shell in the containerized build environment...${NC}\n" 58 | @docker run \ 59 | -ti \ 60 | --rm \ 61 | --entrypoint /bin/bash \ 62 | $(IMAGE):$(TAG) \ 63 | $(CMD) 64 | 65 | # Example: make push VERSION=0.0.2 66 | push: build-prod 67 | @echo "\n${BLUE}Pushing image to GitHub Docker Registry...${NC}\n" 68 | @docker push $(IMAGE):$(VERSION) 69 | 70 | push-latest: push 71 | docker tag $(IMAGE):$(VERSION) $(IMAGE):latest 72 | @docker push $(IMAGE):latest 73 | 74 | version: 75 | @echo $(TAG) 76 | 77 | .PHONY: clean image-clean build-prod push test build set-version 78 | 79 | clean: 80 | rm -rf node_modules/ 81 | 82 | docker-clean: 83 | @docker system prune -f --filter "label=name=$(MODULE)" 84 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # The Inquest Frontend 2 | 3 | This repo has all the information for the frontend (the dashboard & the landing pages). 4 | 5 | ## Commands: 6 | 7 | ``` 8 | yarn run dev 9 | ``` 10 | 11 | runs the frontend in development mode 12 | 13 | 14 | ``` 15 | yarn run test 16 | ``` 17 | 18 | runs tests 19 | 20 | 21 | ``` 22 | yarn run build 23 | ``` 24 | 25 | builds all the static assets 26 | 27 | 28 | ``` 29 | yarn run start 30 | ``` 31 | 32 | runs the server in production mode 33 | (must be run after `yarn run build`) 34 | 35 | -------------------------------------------------------------------------------- /frontend/apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | service: "frontend", 4 | localSchemaFile: "./schema.graphql", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/docker/dev.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.12-buster-slim AS builder 2 | WORKDIR /app 3 | COPY package.json package.json 4 | COPY yarn.lock yarn.lock 5 | RUN yarn install 6 | 7 | FROM builder AS tester 8 | WORKDIR /app 9 | COPY . . 10 | COPY .env.development .env 11 | RUN yarn build 12 | 13 | FROM tester AS runner 14 | WORKDIR /app 15 | 16 | ENV NODE_ENV development 17 | ENTRYPOINT ["/usr/local/bin/yarn", "start"] 18 | 19 | LABEL name={NAME} 20 | LABEL version={VERSION} 21 | -------------------------------------------------------------------------------- /frontend/docker/prod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13.12-buster-slim AS builder 2 | WORKDIR /app 3 | COPY package.json package.json 4 | COPY yarn.lock yarn.lock 5 | RUN yarn install 6 | 7 | COPY . . 8 | RUN yarn build 9 | 10 | FROM node:13.12-buster-slim AS runner 11 | WORKDIR /app 12 | 13 | COPY --from=builder /app/.env /app/.env 14 | COPY --from=builder /app/package.json /app/package.json 15 | COPY --from=builder /app/node_modules /app/node_modules 16 | COPY --from=builder /app/.next /app/.next 17 | COPY --from=builder /app/public /app/public 18 | 19 | ENTRYPOINT ["/usr/local/bin/yarn", "start"] 20 | 21 | LABEL name={NAME} 22 | LABEL version={VERSION} 23 | -------------------------------------------------------------------------------- /frontend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | const withCSS = require("@zeit/next-css"); 2 | const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); 3 | const withTM = require("next-transpile-modules")([ 4 | // `monaco-editor` isn't published to npm correctly: it includes both CSS 5 | // imports and non-Node friendly syntax, so it needs to be compiled. 6 | "monaco-editor", 7 | ]); 8 | 9 | module.exports = withTM({ 10 | webpack: (config) => { 11 | const rule = config.module.rules 12 | .find((rule) => rule.oneOf) 13 | .oneOf.find( 14 | (r) => 15 | // Find the global CSS loader 16 | r.issuer && 17 | r.issuer.include && 18 | r.issuer.include.includes("_app") 19 | ); 20 | if (rule) { 21 | rule.issuer.include = [ 22 | rule.issuer.include, 23 | // Allow `monaco-editor` to import global CSS: 24 | /[\\/]node_modules[\\/]monaco-editor[\\/]/, 25 | ]; 26 | } 27 | 28 | config.plugins.push( 29 | new MonacoWebpackPlugin({ 30 | languages: ["json", "python", "yaml"], 31 | filename: "static/[name].worker.js", 32 | }) 33 | ); 34 | return config; 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | // ... 4 | "postcss-import", 5 | "tailwindcss", 6 | "autoprefixer", 7 | // ... 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /frontend/public/resources/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/frontend/public/resources/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/resources/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/frontend/public/resources/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/public/resources/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/frontend/public/resources/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/public/resources/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/frontend/public/resources/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/resources/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/frontend/public/resources/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/frontend/public/resources/favicon.ico -------------------------------------------------------------------------------- /frontend/public/resources/inquest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/frontend/public/resources/inquest.png -------------------------------------------------------------------------------- /frontend/public/resources/inquest_logging_video.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yiblet/inquest/fa5af52368b86f48ebc816a09ac4b5c609e39bcf/frontend/public/resources/inquest_logging_video.webm -------------------------------------------------------------------------------- /frontend/public/resources/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inquest", 3 | "short_name": "inquest", 4 | "icons": [ 5 | { 6 | "src": "/resources/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/resources/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "rgb(49, 130, 206)", 17 | "background_color": "rgb(49, 130, 206)", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /frontend/schema.graphql: -------------------------------------------------------------------------------- 1 | ../backend/schema.graphql -------------------------------------------------------------------------------- /frontend/src/components/code_view/marker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from "react"; 2 | import * as monacoEditor from "monaco-editor/esm/vs/editor/editor.api"; 3 | import { Editor } from "./utils"; 4 | import { NodeRenderer } from "./node_renderer"; 5 | import { createLogger } from "../../utils/logger"; 6 | 7 | const logger = createLogger(["Marker"]); 8 | 9 | class ContentWidget implements monacoEditor.editor.IContentWidget { 10 | public readonly domNode: HTMLElement; 11 | constructor(private name: string, private line: number) { 12 | this.domNode = document.createElement("div"); 13 | this.domNode.style.minWidth = "40rem"; 14 | } 15 | 16 | getDomNode() { 17 | return this.domNode; 18 | } 19 | 20 | getId() { 21 | return this.name; 22 | } 23 | 24 | get height() { 25 | return this.domNode.offsetHeight; 26 | } 27 | 28 | getPosition() { 29 | return { 30 | position: { 31 | lineNumber: this.line, 32 | column: 5, 33 | }, 34 | range: null, 35 | preference: [ 36 | monacoEditor.editor.ContentWidgetPositionPreference.BELOW, 37 | ], 38 | }; 39 | } 40 | } 41 | 42 | export type MarkerProps = { 43 | id: string; 44 | line: number; 45 | visible: boolean; 46 | editor: Editor; 47 | children: React.ReactElement; 48 | }; 49 | 50 | /** 51 | * Maintains the state of each marker 52 | */ 53 | export const Marker: React.FC = ({ 54 | id, 55 | line, 56 | editor, 57 | children, 58 | visible, 59 | }) => { 60 | const contentWidget = useMemo(() => { 61 | return new ContentWidget(id, line); 62 | }, [id, line, editor]); 63 | 64 | logger.debug(`rerender visibility=${visible}`); 65 | 66 | useEffect(() => { 67 | if (!visible) return; 68 | editor.addContentWidget(contentWidget); 69 | return () => { 70 | editor.removeContentWidget(contentWidget); 71 | }; 72 | }, [contentWidget, visible, editor]); 73 | 74 | return {children}; 75 | }; 76 | -------------------------------------------------------------------------------- /frontend/src/components/code_view/node_renderer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | interface NodeRendererProps { 5 | node: HTMLElement; 6 | children: React.ReactElement; 7 | } 8 | 9 | /** 10 | * RefRenderer 11 | * utility class to let react render components and pass things down to the children 12 | */ 13 | export const NodeRenderer = ({ node, children }: NodeRendererProps) => { 14 | // this will unmount the component if we switch nodes 15 | useEffect(() => { 16 | return () => { 17 | ReactDOM.unmountComponentAtNode(node); 18 | }; 19 | }, [node]); 20 | useEffect(() => { 21 | ReactDOM.render(children, node); 22 | }, [node, children]); 23 | // always render the children however 24 | return <>; 25 | }; 26 | -------------------------------------------------------------------------------- /frontend/src/components/code_view/utils.tsx: -------------------------------------------------------------------------------- 1 | import * as monacoEditor from "monaco-editor/esm/vs/editor/editor.api"; 2 | import { TraceFragment } from "../../generated/TraceFragment"; 3 | 4 | export type Trace = string; 5 | export type FuncName = string; 6 | 7 | export type ExistingTrace = TraceFragment; 8 | 9 | export type Editor = monacoEditor.editor.IStandaloneCodeEditor; 10 | export type Monaco = typeof monacoEditor; 11 | -------------------------------------------------------------------------------- /frontend/src/components/file_tree/file_tree.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Module, ModuleProps } from "./module"; 3 | 4 | export const Line: React.FC<{ 5 | highlight?: boolean; 6 | onClick?: (event: React.MouseEvent) => any; 7 | }> = ({ children, highlight, onClick }) => { 8 | let className = "hover:bg-gray-500 cursor-pointer my-1"; 9 | if (highlight) className = className + " bg-gray-400"; 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | }; 16 | 17 | export function FileTree(props: ModuleProps) { 18 | return ( 19 |
20 |
21 | Modules 22 |
23 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/file_tree/module.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import gql from "graphql-tag"; 3 | import { DirectoryFragment } from "../../generated/DirectoryFragment"; 4 | import { FileFragment } from "../../generated/FileFragment"; 5 | import { SubdirectoryFragment } from "../../generated/SubdirectoryFragment"; 6 | 7 | export const FILE_FRAGMENT = gql` 8 | fragment FileFragment on FileInfo { 9 | id 10 | name 11 | classes { 12 | name 13 | } 14 | functions { 15 | name 16 | } 17 | } 18 | `; 19 | 20 | export type ModuleProps = { 21 | file: React.ComponentType<{ fragment: FileFragment }>; 22 | subdirectory: React.ComponentType<{ fragment: SubdirectoryFragment }>; 23 | fragment: DirectoryFragment; 24 | }; 25 | 26 | export function Module(props: ModuleProps) { 27 | return ( 28 |
29 | {props.fragment.subDirectories.map((subdirectory) => ( 30 | 34 | ))} 35 | {props.fragment.files.map((file) => ( 36 | 37 | ))} 38 |
39 | ); 40 | } 41 | 42 | export const SUBDIRECTORY_FRAGMENT = gql` 43 | fragment SubdirectoryFragment on DirectoryInfo { 44 | id 45 | name 46 | } 47 | `; 48 | 49 | export const DIRECTORY_FRAGMENT = gql` 50 | fragment DirectoryFragment on DirectoryInfo { 51 | subDirectories { 52 | id 53 | name 54 | } 55 | files { 56 | ...FileFragment 57 | } 58 | } 59 | ${FILE_FRAGMENT} 60 | `; 61 | -------------------------------------------------------------------------------- /frontend/src/components/modals/modal_context.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Stack } from "../../utils/collections"; 3 | 4 | export const ModalContext = React.createContext({ 5 | modals: Stack(), 6 | }); 7 | -------------------------------------------------------------------------------- /frontend/src/components/utils/labelled_field.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const LabelledField: React.FC<{ 4 | label: React.ReactChild; 5 | className?: string; 6 | }> = ({ label, className, children }) => ( 7 |
8 | 9 | {children} 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /frontend/src/components/utils/notifications.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, createContext, useContext } from "react"; 2 | import { Observable } from "../../utils/observable"; 3 | import { OrderedMap } from "../../utils/collections"; 4 | 5 | const Card: React.FC = ({ children }) => ( 6 |
7 |
8 |
{children}
9 |
10 | ); 11 | 12 | export type Notification = NonNullable; 13 | type NotificationData = { 14 | notification: Notification; 15 | timeout: number; 16 | }; 17 | 18 | const NotificationContext = createContext>( 19 | new Observable() 20 | ); 21 | 22 | export const NotificationProvider = NotificationContext.Provider; 23 | 24 | export const useNotifications: () => Observable = () => 25 | useContext(NotificationContext); 26 | 27 | export const Notifications: React.FC<{ 28 | timeout?: number; 29 | }> = ({ timeout }) => { 30 | const notifactions = React.useContext(NotificationContext); 31 | const [pendingNotifications, setPendingNotifications] = useState< 32 | OrderedMap 33 | >(OrderedMap()); 34 | 35 | // listen to incoming Notificationsn 36 | useEffect(() => { 37 | const observer = (notification: Notification, order: number) => { 38 | const newTimeout = timeout || 5000; 39 | setPendingNotifications((pendingNotifications) => 40 | pendingNotifications.set(order, { 41 | notification, 42 | timeout: newTimeout, 43 | }) 44 | ); 45 | setTimeout( 46 | () => 47 | setPendingNotifications((notifactions) => 48 | notifactions.remove(order) 49 | ), 50 | newTimeout 51 | ); 52 | }; 53 | notifactions.attach(observer); 54 | return () => notifactions.detach(observer); 55 | }, [setPendingNotifications]); 56 | 57 | return ( 58 |
59 | {pendingNotifications.toArray().map(([key, notifaction]) => ( 60 |
63 | setPendingNotifications((notifications) => 64 | notifications.remove(key) 65 | ) 66 | } 67 | className="mb-4" 68 | > 69 | {notifaction.notification} 70 |
71 | ))} 72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /frontend/src/components/utils/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | 5 | export type TooltipProps = { 6 | width?: string; 7 | floatHorizontal?: "left" | "right"; 8 | floatVertical?: "above" | "below"; 9 | }; 10 | 11 | /** 12 | * this tooltip doesn't wrap the inside with a div 13 | */ 14 | export const RawTooltip: React.FC = ({ 15 | children, 16 | floatVertical, 17 | floatHorizontal, 18 | width, 19 | }) => { 20 | const [isShown, setIsShown] = useState(false); 21 | useEffect(() => { 22 | if (isShown === "out") { 23 | const handler = setTimeout(() => setIsShown(false), 250); 24 | return () => clearTimeout(handler); 25 | } 26 | }, [isShown === "out", setIsShown]); 27 | 28 | if (floatVertical !== undefined) { 29 | floatVertical = "above"; 30 | } 31 | if (floatHorizontal !== undefined) { 32 | floatHorizontal = "right"; 33 | } 34 | 35 | const style: React.CSSProperties = {}; 36 | if (floatVertical === "above") { 37 | style.top = "1.5rem"; 38 | } else { 39 | style.bottom = "1.5rem"; 40 | } 41 | 42 | if (floatHorizontal === "right") { 43 | style.right = "1rem"; 44 | } else { 45 | style.left = "1rem"; 46 | } 47 | 48 | return ( 49 |
setIsShown(true)} 52 | onMouseLeave={() => setIsShown("out")} 53 | > 54 | 55 | {isShown ? ( 56 |
57 |
58 |
59 | {children} 60 |
61 |
62 |
63 | ) : ( 64 | <> 65 | )} 66 |
67 | ); 68 | }; 69 | 70 | /** 71 | * this tooltip wraps the inside with a div 72 | */ 73 | export const Tooltip: React.FC = ({ 74 | children, 75 | floatVertical, 76 | floatHorizontal, 77 | width, 78 | }) => ( 79 | 84 |
85 | {children} 86 |
87 |
88 | ); 89 | -------------------------------------------------------------------------------- /frontend/src/components/utils/with_title.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import React from "react"; 3 | 4 | export const WithTitle: React.FC<{ title: string }> = ({ title, children }) => ( 5 | <> 6 | 7 | {title} 8 | 9 | {children} 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /frontend/src/config.ts: -------------------------------------------------------------------------------- 1 | export type PublicRuntimeConfig = { 2 | endpoint: string; 3 | docsEndpoint: string; 4 | }; 5 | 6 | export function getPublicRuntimeConfig(): PublicRuntimeConfig { 7 | return { 8 | endpoint: 9 | process.env.NEXT_PUBLIC_ENDPOINT || 10 | `${process.env.BACKEND_HOST || "localhost"}:${ 11 | process.env.BACKEND_PORT || 4000 12 | }`, 13 | docsEndpoint: 14 | process.env.NEXT_PUBLIC_DOCS_ENDPOINT || "docs.inquest.dev", 15 | }; 16 | } 17 | 18 | export type ServerRuntimeConfig = { 19 | endpoint: string; 20 | }; 21 | 22 | export function getServerRuntimeConfig(): ServerRuntimeConfig { 23 | return { 24 | endpoint: `${process.env.BACKEND_HOST || "localhost"}:${ 25 | process.env.BACKEND_PORT || 4000 26 | }`, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/connectors/live_tail.connector.tsx: -------------------------------------------------------------------------------- 1 | import { gql, useSubscription } from "@apollo/client"; 2 | import { LiveTailSubscription } from "../generated/LiveTailSubscription"; 3 | import React, { useState, useEffect, useCallback } from "react"; 4 | import { LiveTail } from "../components/live_tail/live_tail"; 5 | import { List } from "immutable"; 6 | 7 | const LIVE_TAIL_SUBSCRIPTION = gql` 8 | subscription LiveTailSubscription($traceSetId: String!) { 9 | listenLog(traceSetId: $traceSetId) 10 | } 11 | `; 12 | 13 | /** 14 | * shows the user's live tail 15 | */ 16 | export const LiveTailConnector: React.FC<{ traceSetId: string }> = ({ 17 | traceSetId, 18 | }) => { 19 | const { data } = useSubscription( 20 | LIVE_TAIL_SUBSCRIPTION, 21 | { 22 | variables: { traceSetId }, 23 | } 24 | ); 25 | 26 | const [logs, setLogs] = useState>(List()); 27 | useEffect(() => { 28 | data?.listenLog && setLogs((logs) => logs.push(...data.listenLog)); 29 | }, [data?.listenLog, setLogs]); 30 | const clearLogs = useCallback(() => setLogs(() => List()), [logs, setLogs]); 31 | 32 | return ; 33 | }; 34 | -------------------------------------------------------------------------------- /frontend/src/generated/CodeViewFragment.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL fragment: CodeViewFragment 8 | // ==================================================== 9 | 10 | export interface CodeViewFragment_functions_traces_currentFailures { 11 | readonly __typename: "ProbeFailure"; 12 | readonly message: string; 13 | } 14 | 15 | export interface CodeViewFragment_functions_traces { 16 | readonly __typename: "Trace"; 17 | readonly id: string; 18 | readonly statement: string; 19 | readonly line: number; 20 | readonly active: boolean; 21 | readonly version: number; 22 | readonly currentFailures: ReadonlyArray; 23 | } 24 | 25 | export interface CodeViewFragment_functions { 26 | readonly __typename: "FunctionInfo"; 27 | readonly id: string; 28 | readonly startLine: number; 29 | readonly endLine: number; 30 | readonly name: string; 31 | readonly traces: ReadonlyArray; 32 | } 33 | 34 | export interface CodeViewFragment_classes_methods_traces_currentFailures { 35 | readonly __typename: "ProbeFailure"; 36 | readonly message: string; 37 | } 38 | 39 | export interface CodeViewFragment_classes_methods_traces { 40 | readonly __typename: "Trace"; 41 | readonly id: string; 42 | readonly statement: string; 43 | readonly line: number; 44 | readonly active: boolean; 45 | readonly version: number; 46 | readonly currentFailures: ReadonlyArray; 47 | } 48 | 49 | export interface CodeViewFragment_classes_methods { 50 | readonly __typename: "FunctionInfo"; 51 | readonly id: string; 52 | readonly startLine: number; 53 | readonly endLine: number; 54 | readonly name: string; 55 | readonly traces: ReadonlyArray; 56 | } 57 | 58 | export interface CodeViewFragment_classes { 59 | readonly __typename: "ClassInfo"; 60 | readonly id: string; 61 | readonly startLine: number; 62 | readonly endLine: number; 63 | readonly name: string; 64 | readonly methods: ReadonlyArray; 65 | } 66 | 67 | export interface CodeViewFragment { 68 | readonly __typename: "FileInfo"; 69 | readonly id: string; 70 | readonly name: string; 71 | readonly content: string; 72 | readonly functions: ReadonlyArray; 73 | readonly classes: ReadonlyArray; 74 | } 75 | -------------------------------------------------------------------------------- /frontend/src/generated/CodeViewQuery.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL query operation: CodeViewQuery 8 | // ==================================================== 9 | 10 | export interface CodeViewQuery_file_functions_traces_currentFailures { 11 | readonly __typename: "ProbeFailure"; 12 | readonly message: string; 13 | } 14 | 15 | export interface CodeViewQuery_file_functions_traces { 16 | readonly __typename: "Trace"; 17 | readonly id: string; 18 | readonly statement: string; 19 | readonly line: number; 20 | readonly active: boolean; 21 | readonly version: number; 22 | readonly currentFailures: ReadonlyArray; 23 | } 24 | 25 | export interface CodeViewQuery_file_functions { 26 | readonly __typename: "FunctionInfo"; 27 | readonly id: string; 28 | readonly startLine: number; 29 | readonly endLine: number; 30 | readonly name: string; 31 | readonly traces: ReadonlyArray; 32 | } 33 | 34 | export interface CodeViewQuery_file_classes_methods_traces_currentFailures { 35 | readonly __typename: "ProbeFailure"; 36 | readonly message: string; 37 | } 38 | 39 | export interface CodeViewQuery_file_classes_methods_traces { 40 | readonly __typename: "Trace"; 41 | readonly id: string; 42 | readonly statement: string; 43 | readonly line: number; 44 | readonly active: boolean; 45 | readonly version: number; 46 | readonly currentFailures: ReadonlyArray; 47 | } 48 | 49 | export interface CodeViewQuery_file_classes_methods { 50 | readonly __typename: "FunctionInfo"; 51 | readonly id: string; 52 | readonly startLine: number; 53 | readonly endLine: number; 54 | readonly name: string; 55 | readonly traces: ReadonlyArray; 56 | } 57 | 58 | export interface CodeViewQuery_file_classes { 59 | readonly __typename: "ClassInfo"; 60 | readonly id: string; 61 | readonly startLine: number; 62 | readonly endLine: number; 63 | readonly name: string; 64 | readonly methods: ReadonlyArray; 65 | } 66 | 67 | export interface CodeViewQuery_file { 68 | readonly __typename: "FileInfo"; 69 | readonly id: string; 70 | readonly name: string; 71 | readonly content: string; 72 | readonly functions: ReadonlyArray; 73 | readonly classes: ReadonlyArray; 74 | } 75 | 76 | export interface CodeViewQuery { 77 | readonly file: CodeViewQuery_file | null; 78 | } 79 | 80 | export interface CodeViewQueryVariables { 81 | readonly fileId: string; 82 | } 83 | -------------------------------------------------------------------------------- /frontend/src/generated/DeleteTraceMutation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: DeleteTraceMutation 8 | // ==================================================== 9 | 10 | export interface DeleteTraceMutation_deleteTrace { 11 | readonly __typename: "Trace"; 12 | readonly id: string; 13 | } 14 | 15 | export interface DeleteTraceMutation { 16 | readonly deleteTrace: DeleteTraceMutation_deleteTrace; 17 | } 18 | 19 | export interface DeleteTraceMutationVariables { 20 | readonly id: string; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/generated/DirectoryFragment.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL fragment: DirectoryFragment 8 | // ==================================================== 9 | 10 | export interface DirectoryFragment_subDirectories { 11 | readonly __typename: "DirectoryInfo"; 12 | readonly id: string; 13 | readonly name: string; 14 | } 15 | 16 | export interface DirectoryFragment_files_classes { 17 | readonly __typename: "ClassInfo"; 18 | readonly name: string; 19 | } 20 | 21 | export interface DirectoryFragment_files_functions { 22 | readonly __typename: "FunctionInfo"; 23 | readonly name: string; 24 | } 25 | 26 | export interface DirectoryFragment_files { 27 | readonly __typename: "FileInfo"; 28 | readonly id: string; 29 | readonly name: string; 30 | readonly classes: ReadonlyArray; 31 | readonly functions: ReadonlyArray; 32 | } 33 | 34 | export interface DirectoryFragment { 35 | readonly __typename: "DirectoryInfo"; 36 | readonly subDirectories: ReadonlyArray; 37 | readonly files: ReadonlyArray; 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/generated/FileFragment.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL fragment: FileFragment 8 | // ==================================================== 9 | 10 | export interface FileFragment_classes { 11 | readonly __typename: "ClassInfo"; 12 | readonly name: string; 13 | } 14 | 15 | export interface FileFragment_functions { 16 | readonly __typename: "FunctionInfo"; 17 | readonly name: string; 18 | } 19 | 20 | export interface FileFragment { 21 | readonly __typename: "FileInfo"; 22 | readonly id: string; 23 | readonly name: string; 24 | readonly classes: ReadonlyArray; 25 | readonly functions: ReadonlyArray; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/generated/FileTreeFragment.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL fragment: FileTreeFragment 8 | // ==================================================== 9 | 10 | export interface FileTreeFragment_rootDirectory_subDirectories { 11 | readonly __typename: "DirectoryInfo"; 12 | readonly id: string; 13 | readonly name: string; 14 | } 15 | 16 | export interface FileTreeFragment_rootDirectory_files_classes { 17 | readonly __typename: "ClassInfo"; 18 | readonly name: string; 19 | } 20 | 21 | export interface FileTreeFragment_rootDirectory_files_functions { 22 | readonly __typename: "FunctionInfo"; 23 | readonly name: string; 24 | } 25 | 26 | export interface FileTreeFragment_rootDirectory_files { 27 | readonly __typename: "FileInfo"; 28 | readonly id: string; 29 | readonly name: string; 30 | readonly classes: ReadonlyArray; 31 | readonly functions: ReadonlyArray; 32 | } 33 | 34 | export interface FileTreeFragment_rootDirectory { 35 | readonly __typename: "DirectoryInfo"; 36 | readonly subDirectories: ReadonlyArray; 37 | readonly files: ReadonlyArray; 38 | } 39 | 40 | export interface FileTreeFragment { 41 | readonly __typename: "TraceSet"; 42 | readonly rootDirectory: FileTreeFragment_rootDirectory; 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/generated/FunctionFragment.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL fragment: FunctionFragment 8 | // ==================================================== 9 | 10 | export interface FunctionFragment_traces_currentFailures { 11 | readonly __typename: "ProbeFailure"; 12 | readonly message: string; 13 | } 14 | 15 | export interface FunctionFragment_traces { 16 | readonly __typename: "Trace"; 17 | readonly id: string; 18 | readonly statement: string; 19 | readonly line: number; 20 | readonly active: boolean; 21 | readonly version: number; 22 | readonly currentFailures: ReadonlyArray; 23 | } 24 | 25 | export interface FunctionFragment { 26 | readonly __typename: "FunctionInfo"; 27 | readonly id: string; 28 | readonly startLine: number; 29 | readonly endLine: number; 30 | readonly name: string; 31 | readonly traces: ReadonlyArray; 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/generated/LiveProbesFragment.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL fragment: LiveProbesFragment 8 | // ==================================================== 9 | 10 | export interface LiveProbesFragment_liveProbes { 11 | readonly __typename: "Probe"; 12 | readonly id: string; 13 | } 14 | 15 | export interface LiveProbesFragment { 16 | readonly __typename: "TraceSet"; 17 | readonly id: string; 18 | readonly liveProbes: ReadonlyArray | null; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/generated/LiveTailSubscription.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL subscription operation: LiveTailSubscription 8 | // ==================================================== 9 | 10 | export interface LiveTailSubscription { 11 | readonly listenLog: ReadonlyArray; 12 | } 13 | 14 | export interface LiveTailSubscriptionVariables { 15 | readonly traceSetId: string; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/generated/NewTraceMutation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | import { NewTraceInput } from "./globalTypes"; 7 | 8 | // ==================================================== 9 | // GraphQL mutation operation: NewTraceMutation 10 | // ==================================================== 11 | 12 | export interface NewTraceMutation_newTrace_currentFailures { 13 | readonly __typename: "ProbeFailure"; 14 | readonly message: string; 15 | } 16 | 17 | export interface NewTraceMutation_newTrace { 18 | readonly __typename: "Trace"; 19 | readonly id: string; 20 | readonly statement: string; 21 | readonly line: number; 22 | readonly active: boolean; 23 | readonly version: number; 24 | readonly currentFailures: ReadonlyArray; 25 | } 26 | 27 | export interface NewTraceMutation { 28 | readonly newTrace: NewTraceMutation_newTrace; 29 | } 30 | 31 | export interface NewTraceMutationVariables { 32 | readonly input: NewTraceInput; 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/generated/RemoveRootDirectoryMutation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: RemoveRootDirectoryMutation 8 | // ==================================================== 9 | 10 | export interface RemoveRootDirectoryMutation { 11 | /** 12 | * returns true if the root directory was removed false if it didn't exist in the first place 13 | */ 14 | readonly removeRootDirectory: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/generated/SubdirectoryFragment.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL fragment: SubdirectoryFragment 8 | // ==================================================== 9 | 10 | export interface SubdirectoryFragment { 11 | readonly __typename: "DirectoryInfo"; 12 | readonly id: string; 13 | readonly name: string; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/generated/SubdirectoryQuery.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL query operation: SubdirectoryQuery 8 | // ==================================================== 9 | 10 | export interface SubdirectoryQuery_directory_subDirectories { 11 | readonly __typename: "DirectoryInfo"; 12 | readonly id: string; 13 | readonly name: string; 14 | } 15 | 16 | export interface SubdirectoryQuery_directory_files_classes { 17 | readonly __typename: "ClassInfo"; 18 | readonly name: string; 19 | } 20 | 21 | export interface SubdirectoryQuery_directory_files_functions { 22 | readonly __typename: "FunctionInfo"; 23 | readonly name: string; 24 | } 25 | 26 | export interface SubdirectoryQuery_directory_files { 27 | readonly __typename: "FileInfo"; 28 | readonly id: string; 29 | readonly name: string; 30 | readonly classes: ReadonlyArray; 31 | readonly functions: ReadonlyArray; 32 | } 33 | 34 | export interface SubdirectoryQuery_directory { 35 | readonly __typename: "DirectoryInfo"; 36 | readonly subDirectories: ReadonlyArray; 37 | readonly files: ReadonlyArray; 38 | } 39 | 40 | export interface SubdirectoryQuery { 41 | readonly directory: SubdirectoryQuery_directory | null; 42 | } 43 | 44 | export interface SubdirectoryQueryVariables { 45 | readonly directoryId: string; 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/generated/TraceFragment.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL fragment: TraceFragment 8 | // ==================================================== 9 | 10 | export interface TraceFragment_currentFailures { 11 | readonly __typename: "ProbeFailure"; 12 | readonly message: string; 13 | } 14 | 15 | export interface TraceFragment { 16 | readonly __typename: "Trace"; 17 | readonly id: string; 18 | readonly statement: string; 19 | readonly line: number; 20 | readonly active: boolean; 21 | readonly version: number; 22 | readonly currentFailures: ReadonlyArray; 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/generated/UpdateTraceMutation.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL mutation operation: UpdateTraceMutation 8 | // ==================================================== 9 | 10 | export interface UpdateTraceMutation_updateTrace_currentFailures { 11 | readonly __typename: "ProbeFailure"; 12 | readonly message: string; 13 | } 14 | 15 | export interface UpdateTraceMutation_updateTrace { 16 | readonly __typename: "Trace"; 17 | readonly id: string; 18 | readonly statement: string; 19 | readonly line: number; 20 | readonly active: boolean; 21 | readonly version: number; 22 | readonly currentFailures: ReadonlyArray; 23 | } 24 | 25 | export interface UpdateTraceMutation { 26 | readonly updateTrace: UpdateTraceMutation_updateTrace; 27 | } 28 | 29 | export interface UpdateTraceMutationVariables { 30 | readonly active?: boolean | null; 31 | readonly statement?: string | null; 32 | readonly id: string; 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/generated/UserContextQuery.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL query operation: UserContextQuery 8 | // ==================================================== 9 | 10 | export interface UserContextQuery_me_organization_traceSets_liveProbes { 11 | readonly __typename: "Probe"; 12 | readonly id: string; 13 | } 14 | 15 | export interface UserContextQuery_me_organization_traceSets_rootDirectory_subDirectories { 16 | readonly __typename: "DirectoryInfo"; 17 | readonly id: string; 18 | readonly name: string; 19 | } 20 | 21 | export interface UserContextQuery_me_organization_traceSets_rootDirectory_files_classes { 22 | readonly __typename: "ClassInfo"; 23 | readonly name: string; 24 | } 25 | 26 | export interface UserContextQuery_me_organization_traceSets_rootDirectory_files_functions { 27 | readonly __typename: "FunctionInfo"; 28 | readonly name: string; 29 | } 30 | 31 | export interface UserContextQuery_me_organization_traceSets_rootDirectory_files { 32 | readonly __typename: "FileInfo"; 33 | readonly id: string; 34 | readonly name: string; 35 | readonly classes: ReadonlyArray; 36 | readonly functions: ReadonlyArray; 37 | } 38 | 39 | export interface UserContextQuery_me_organization_traceSets_rootDirectory { 40 | readonly __typename: "DirectoryInfo"; 41 | readonly subDirectories: ReadonlyArray; 42 | readonly files: ReadonlyArray; 43 | } 44 | 45 | export interface UserContextQuery_me_organization_traceSets { 46 | readonly __typename: "TraceSet"; 47 | readonly id: string; 48 | readonly liveProbes: ReadonlyArray | null; 49 | readonly rootDirectory: UserContextQuery_me_organization_traceSets_rootDirectory; 50 | } 51 | 52 | export interface UserContextQuery_me_organization { 53 | readonly __typename: "Organization"; 54 | readonly traceSets: ReadonlyArray; 55 | } 56 | 57 | export interface UserContextQuery_me { 58 | readonly __typename: "User"; 59 | readonly organization: UserContextQuery_me_organization; 60 | } 61 | 62 | export interface UserContextQuery { 63 | readonly me: UserContextQuery_me | null; 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/generated/globalTypes.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | //============================================================== 7 | // START Enums and Input Objects 8 | //============================================================== 9 | 10 | export interface NewTraceInput { 11 | readonly functionId: string; 12 | readonly line: number; 13 | readonly statement: string; 14 | readonly traceSetId: string; 15 | } 16 | 17 | //============================================================== 18 | // END Enums and Input Objects 19 | //============================================================== 20 | -------------------------------------------------------------------------------- /frontend/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/style.css"; 2 | import "@fortawesome/fontawesome-svg-core/styles.css"; 3 | import React, { useState, useEffect } from "react"; 4 | import Head from "next/head"; 5 | import { 6 | Notification, 7 | NotificationProvider, 8 | Notifications, 9 | } from "../components/utils/notifications"; 10 | import { Observable } from "../utils/observable"; 11 | import { config } from "@fortawesome/fontawesome-svg-core"; 12 | import { gaService } from "../services/ga_service"; 13 | 14 | // Prevent fontawesome from adding its CSS since we did it manually above: 15 | config.autoAddCss = false; 16 | 17 | function MyApp({ Component, pageProps }) { 18 | const [observable] = useState>(new Observable()); 19 | 20 | useEffect(() => { 21 | gaService.initialize(); 22 | gaService.logPageView(); 23 | }); 24 | 25 | return ( 26 | 27 | 28 | 29 | Inquest - Get Vision On Your Production Code Instantly 30 | 31 | 35 | 39 | 40 | 41 | 45 | 49 | 50 | 55 | 56 | 60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | 67 | export default MyApp; 68 | -------------------------------------------------------------------------------- /frontend/src/pages/privacy_policy.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Layout from "../components/utils/layout"; 3 | 4 | const Policy: React.FC = () => ( 5 | 6 |
7 |

Privacy Policy

8 |

9 | Your privacy is important to us. It is Inquest's policy to 10 | respect your privacy regarding any information we may collect 11 | from you across our website,{" "} 12 | https://inquest.dev, and other 13 | sites we own and operate. 14 |

15 |

16 | We only ask for personal information when we truly need it to 17 | provide a service to you. We collect it by fair and lawful 18 | means, with your knowledge and consent. We also let you know why 19 | we’re collecting it and how it will be used. 20 |

21 |

22 | We only retain collected information for as long as necessary to 23 | provide you with your requested service. What data we store, 24 | we’ll protect within commercially acceptable means to prevent 25 | loss and theft, as well as unauthorized access, disclosure, 26 | copying, use or modification. 27 |

28 |

29 | We don’t share any personally identifying information publicly 30 | or with third-parties, except when required to by law. 31 |

32 |

33 | Our website may link to external sites that are not operated by 34 | us. Please be aware that we have no control over the content and 35 | practices of these sites, and cannot accept responsibility or 36 | liability for their respective privacy policies. 37 |

38 |

39 | You are free to refuse our request for your personal 40 | information, with the understanding that we may be unable to 41 | provide you with some of your desired services. 42 |

43 |

44 | Your continued use of our website will be regarded as acceptance 45 | of our practices around privacy and personal information. If you 46 | have any questions about how we handle user data and personal 47 | information, feel free to contact us. 48 |

49 |

This policy is effective as of 28 May 2020.

50 |
51 |
52 | ); 53 | 54 | export default Policy; 55 | -------------------------------------------------------------------------------- /frontend/src/services/ga_service.ts: -------------------------------------------------------------------------------- 1 | import * as ReactGA from "react-ga"; 2 | 3 | class GAService { 4 | public readonly ga: typeof ReactGA; 5 | constructor() { 6 | this.ga = ReactGA; 7 | } 8 | 9 | initialize() { 10 | this.ga.initialize("UA-78950781-7"); 11 | } 12 | 13 | logPageView() { 14 | this.ga.set({ page: window.location.pathname }); 15 | this.ga.pageview(window.location.pathname); 16 | } 17 | 18 | wrapWithGa( 19 | func: (value: T) => R, 20 | extractor: (value: T) => string, 21 | category = "user" 22 | ): (value: T) => R { 23 | return (value: T) => { 24 | this.ga.event({ 25 | category, 26 | action: extractor(value), 27 | }); 28 | return func(value); 29 | }; 30 | } 31 | } 32 | 33 | export const gaService = new GAService(); 34 | -------------------------------------------------------------------------------- /frontend/src/styles/article.css: -------------------------------------------------------------------------------- 1 | .article { 2 | @apply text-gray-900 leading-normal break-words; 3 | } 4 | 5 | .article > * + * { 6 | @apply mt-0 mb-4; 7 | } 8 | 9 | .article li + li { 10 | @apply mt-1; 11 | } 12 | 13 | .article li > p + p { 14 | @apply mt-6; 15 | } 16 | 17 | .article strong { 18 | @apply font-semibold; 19 | } 20 | 21 | .article a { 22 | @apply text-blue-600 font-semibold; 23 | } 24 | 25 | .article strong a { 26 | @apply font-bold; 27 | } 28 | 29 | .article h1 { 30 | @apply leading-tight border-b text-4xl font-semibold mb-4 mt-6 pb-2; 31 | } 32 | 33 | .article h2 { 34 | @apply leading-tight border-b text-2xl font-semibold mb-4 mt-6 pb-2; 35 | } 36 | 37 | .article h3 { 38 | @apply leading-snug text-lg font-semibold mb-4 mt-6; 39 | } 40 | 41 | .article h4 { 42 | @apply leading-none text-base font-semibold mb-4 mt-6; 43 | } 44 | 45 | .article h5 { 46 | @apply leading-tight text-sm font-semibold mb-4 mt-6; 47 | } 48 | 49 | .article h6 { 50 | @apply leading-tight text-sm font-semibold text-gray-600 mb-4 mt-6; 51 | } 52 | 53 | .article blockquote { 54 | @apply text-base border-l-4 border-gray-300 pl-4 pr-4 text-gray-600; 55 | } 56 | 57 | .article code { 58 | @apply font-mono text-sm inline bg-gray-200 rounded px-1 py-1; 59 | } 60 | 61 | .article pre { 62 | @apply bg-gray-100 rounded p-4; 63 | } 64 | 65 | .article pre code { 66 | @apply block bg-transparent p-0 overflow-visible rounded-none; 67 | } 68 | 69 | .article ul { 70 | @apply text-base pl-8 list-disc; 71 | } 72 | 73 | .article ol { 74 | @apply text-base pl-8 list-decimal; 75 | } 76 | 77 | .article kbd { 78 | @apply text-xs inline-block rounded border px-1 py-1 align-middle font-normal font-mono shadow; 79 | } 80 | 81 | .article table { 82 | @apply text-base border-gray-600; 83 | } 84 | 85 | .article th { 86 | @apply border py-1 px-3; 87 | } 88 | 89 | .article td { 90 | @apply border py-1 px-3; 91 | } 92 | 93 | /* Override pygments style background color. */ 94 | .article .highlight pre { 95 | @apply bg-gray-100 !important; 96 | } 97 | -------------------------------------------------------------------------------- /frontend/src/styles/style.css: -------------------------------------------------------------------------------- 1 | @import "article.css"; 2 | 3 | @tailwind base; /* Preflight will be injected here */ 4 | 5 | @tailwind components; 6 | 7 | @tailwind utilities; 8 | 9 | .logo { 10 | font-family: "Lato", sans-serif; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/utils/__tests__/collections_tests.ts: -------------------------------------------------------------------------------- 1 | import { List, ImmMap } from "../collections"; 2 | 3 | it("test equality", () => { 4 | expect(ImmMap()).toEqual(ImmMap()); 5 | }); 6 | 7 | it("test list", () => { 8 | const list1 = List([1, 2, 3]); 9 | expect(list1.filter((x) => x % 2 !== 0)).toEqual( 10 | list1.filter((x) => x % 2 !== 0) 11 | ); 12 | expect(list1.filter((x) => x % 2 !== 0).push(2)).toEqual( 13 | list1.filter((x) => x % 2 !== 0).push(2) 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/utils/apollo_client.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client"; 2 | import { WebSocketLink } from "@apollo/link-ws"; 3 | import { SubscriptionClient } from "subscriptions-transport-ws"; 4 | import fetch from "isomorphic-unfetch"; 5 | import { getPublicRuntimeConfig, getServerRuntimeConfig } from "../config"; 6 | import { isSecure } from "./protocol"; 7 | 8 | /** 9 | * generates a websocket connection with the user client 10 | */ 11 | export function createApolloClient(token) { 12 | const secure = isSecure(); 13 | const ssrMode = !process.browser; 14 | // The `ctx` (NextPageContext) will only be present on the server. 15 | // use it to extract auth headers (ctx.req) or similar. 16 | 17 | let link; 18 | if (ssrMode) { 19 | link = new HttpLink({ 20 | uri: `http${secure ? "s" : ""}://${ 21 | getServerRuntimeConfig().endpoint 22 | }/api/graphql`, // Server URL (must be absolute) 23 | credentials: "same-origin", // Additional fetch() options like `credentials` or `headers` 24 | headers: { 25 | "X-Token": token, 26 | }, 27 | fetch, 28 | }); 29 | } else { 30 | const client = new SubscriptionClient( 31 | `ws${secure ? "s" : ""}://${ 32 | getPublicRuntimeConfig().endpoint 33 | }/api/graphql`, 34 | { 35 | reconnect: true, 36 | connectionParams: { 37 | token, 38 | }, 39 | } 40 | ); 41 | link = new WebSocketLink(client); 42 | } 43 | 44 | return new ApolloClient({ 45 | ssrMode, 46 | link, 47 | cache: new InMemoryCache(), 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/utils/auth.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { NextPageContext } from "next"; 3 | import { useEffect } from "react"; 4 | import Router, { useRouter } from "next/router"; 5 | import { Cookie } from "next-cookie"; 6 | import cookie from "js-cookie"; 7 | import { useNotifications } from "../components/utils/notifications"; 8 | 9 | /** 10 | * logs the user in 11 | */ 12 | export const login = (token: string) => { 13 | cookie.set("token", token, { expires: 3 }); 14 | Router.replace("/dashboard"); 15 | }; 16 | 17 | /** 18 | * retrieves the user's JWT token 19 | */ 20 | export const getToken = () => { 21 | return cookie.get("token"); 22 | }; 23 | 24 | /** 25 | * returns undefined if loggedIn is not known (on first render) 26 | */ 27 | export const useLoggedInState = (): boolean | undefined => { 28 | const [loggedIn, setLoggedIn] = useState(undefined); 29 | useEffect(() => { 30 | setLoggedIn(getToken() != undefined); 31 | }); 32 | return loggedIn; 33 | }; 34 | 35 | /** 36 | * redirects if the user is not logged in 37 | */ 38 | export const useEnsureLoggedIn = () => { 39 | const notifications = useNotifications(); 40 | const loggedIn = useLoggedInState(); 41 | const router = useRouter(); 42 | useEffect(() => { 43 | if (loggedIn === false) { 44 | notifications.notify("need to log in"); 45 | router.replace("/login"); 46 | } 47 | }, [loggedIn !== undefined]); // effect is only fired off twice 48 | // once when loggedIn === undefined (at first render) 49 | // second when loggedIn is a boolean (after the first render) 50 | }; 51 | 52 | /** 53 | * redirects of the user is already logged in 54 | */ 55 | export const useEnsureNotLoggedIn = (redirectLocation: string) => { 56 | const notifications = useNotifications(); 57 | const loggedIn = useLoggedInState(); 58 | const router = useRouter(); 59 | useEffect(() => { 60 | if (loggedIn === true) { 61 | notifications.notify("already logged in"); 62 | router.replace(redirectLocation); 63 | } 64 | }, [loggedIn !== undefined]); // effect is only fired off twice 65 | // once when loggedIn === undefined (at first render) 66 | // second when loggedIn is a boolean (after the first render) 67 | }; 68 | 69 | export const auth = (ctx: NextPageContext) => { 70 | const cookie = new Cookie(ctx); 71 | const token = cookie.get("token"); 72 | // If there's no token, it means the user is not logged in. 73 | if (!token) { 74 | if (!process.browser) { 75 | ctx.res?.writeHead(302, { Location: "/login" }); 76 | ctx.res?.end(); 77 | } else { 78 | Router.replace("/login"); 79 | } 80 | } 81 | return token; 82 | }; 83 | 84 | /** 85 | * logs the user out 86 | * TODO set up a way to ensure other tabs are also logged out of 87 | */ 88 | export const logout = () => { 89 | cookie.remove("token"); 90 | if (window) window.localStorage.setItem("logout", `${Date.now()}`); 91 | Router.replace("/login"); 92 | }; 93 | -------------------------------------------------------------------------------- /frontend/src/utils/clipboard_copy.ts: -------------------------------------------------------------------------------- 1 | export const copyToClipboard = (str: string) => { 2 | const el = document.createElement("textarea"); 3 | el.value = str; 4 | el.setAttribute("readonly", ""); 5 | el.style.position = "absolute"; 6 | el.style.left = "-9999px"; 7 | document.body.appendChild(el); 8 | el.select(); 9 | document.execCommand("copy"); 10 | document.body.removeChild(el); 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/src/utils/collections.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Set as ImmSet, 3 | Map as ImmMap, 4 | Record as ImmRecord, 5 | List, 6 | Stack, 7 | Seq, 8 | OrderedSet, 9 | OrderedMap, 10 | } from "immutable"; 11 | -------------------------------------------------------------------------------- /frontend/src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | type Parameters any> = T extends ( 2 | ...args: infer P 3 | ) => any 4 | ? P 5 | : never; 6 | 7 | export function debounce void>( 8 | func: T, 9 | waitTime = 250 10 | ): (...funcArgs: Parameters) => void { 11 | let time: number | null = null; 12 | return (...args: Parameters): void => { 13 | const now = Date.now(); 14 | if (time === null || now - time > waitTime) { 15 | time = now; 16 | func(...args); 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | // TODO move to an actual logging framework 2 | 3 | export interface Logger { 4 | debug(message: string, tags?: string[]); 5 | info(message: string, tags?: string[]); 6 | warn(message: string, tags?: string[]); 7 | error(message: string, tags?: string[]); 8 | } 9 | 10 | function convertMessage(message: string, tags: string[]) { 11 | return `${tags.map((tag) => `[${tag}]`).join("")} ${message}`; 12 | } 13 | 14 | export class MainLogger implements Logger { 15 | sendMessage(message: string, logger: (string) => any) { 16 | logger(message); 17 | } 18 | debug(message: string, tags: string[] = []) { 19 | this.sendMessage( 20 | convertMessage(message, ["DEBUG"].concat(tags)), 21 | console.debug 22 | ); 23 | } 24 | info(message: string, tags: string[] = []) { 25 | this.sendMessage( 26 | convertMessage(message, ["INFO"].concat(tags)), 27 | console.info 28 | ); 29 | } 30 | warn(message: string, tags: string[] = []) { 31 | this.sendMessage( 32 | convertMessage(message, ["WARNING"].concat(tags)), 33 | console.warn 34 | ); 35 | } 36 | error(message: string, tags: string[] = []) { 37 | this.sendMessage( 38 | convertMessage(message, ["ERROR"].concat(tags)), 39 | console.error 40 | ); 41 | } 42 | } 43 | 44 | export class TagLogger implements Logger { 45 | constructor(private base: Logger, private tags: string[]) {} 46 | debug(message: string, tags: string[] = []) { 47 | this.base.debug(message, this.tags.concat(tags)); 48 | } 49 | info(message: string, tags: string[] = []) { 50 | this.base.info(message, this.tags.concat(tags)); 51 | } 52 | warn(message: string, tags: string[] = []) { 53 | this.base.warn(message, this.tags.concat(tags)); 54 | } 55 | error(message: string, tags: string[] = []) { 56 | this.base.error(message, this.tags.concat(tags)); 57 | } 58 | } 59 | 60 | const logger = new MainLogger(); 61 | 62 | export function createLogger( 63 | tags: string[], 64 | loggerInstance: Logger = logger 65 | ): Logger { 66 | return new TagLogger(logger, tags); 67 | } 68 | -------------------------------------------------------------------------------- /frontend/src/utils/observable.ts: -------------------------------------------------------------------------------- 1 | import { OrderedSet } from "./collections"; 2 | 3 | export type Observer = (value: T, order: number) => any; 4 | 5 | /** 6 | * A Observer class 7 | */ 8 | export class Observable { 9 | private observers: OrderedSet> = OrderedSet(); 10 | private order = 0; 11 | 12 | public attach(observer: Observer) { 13 | this.observers = this.observers.add(observer); 14 | } 15 | 16 | public detach(observer: Observer) { 17 | this.observers = this.observers.remove(observer); 18 | } 19 | 20 | public notify(value: T) { 21 | this.observers.forEach((obv) => obv(value, this.order)); 22 | this.order++; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/utils/partial.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Subtract> = { 4 | [K in Exclude]: 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 --------------------------------------------------------------------------------