├── silk ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── silk_clear_request_log.py │ │ └── silk_request_garbage_collect.py ├── migrations │ ├── __init__.py │ ├── 0008_sqlquery_analysis.py │ ├── 0007_sqlquery_identifier.py │ ├── 0003_request_prof_file.py │ ├── 0004_request_prof_file_storage.py │ ├── 0005_increase_request_prof_file_length.py │ ├── 0006_fix_request_prof_file_blank.py │ └── 0002_auto_update_uuid4_id_field.py ├── utils │ ├── __init__.py │ ├── pagination.py │ ├── profile_parser.py │ └── data_deletion.py ├── views │ ├── __init__.py │ ├── cprofile.py │ ├── profile_download.py │ ├── raw.py │ ├── code.py │ ├── profile_detail.py │ ├── clear_db.py │ ├── sql.py │ ├── request_detail.py │ ├── profile_dot.py │ └── sql_detail.py ├── profiling │ └── __init__.py ├── code_generation │ ├── __init__.py │ ├── django_test_client.py │ └── curl.py ├── templatetags │ ├── __init__.py │ ├── silk_nav.py │ ├── silk_urls.py │ ├── silk_inclusion.py │ └── silk_filters.py ├── static │ └── silk │ │ ├── js │ │ ├── pages │ │ │ ├── raw.js │ │ │ ├── request.js │ │ │ ├── detail_base.js │ │ │ ├── profiling.js │ │ │ ├── requests.js │ │ │ ├── summary.js │ │ │ ├── clear_db.js │ │ │ ├── sql_detail.js │ │ │ ├── base.js │ │ │ ├── root_base.js │ │ │ ├── sql.js │ │ │ └── profile_detail.js │ │ └── components │ │ │ ├── cell.js │ │ │ └── filters.js │ │ ├── filter.png │ │ ├── filter2.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── fonts │ │ ├── fira │ │ │ ├── FiraSans-Bold.woff │ │ │ ├── FiraSans-Light.woff │ │ │ ├── FiraSans-Medium.woff │ │ │ ├── FiraSans-Regular.woff │ │ │ ├── FiraSans-BoldItalic.woff │ │ │ ├── FiraSans-LightItalic.woff │ │ │ ├── FiraSans-MediumItalic.woff │ │ │ └── FiraSans-RegularItalic.woff │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ ├── glyphicons-halflings-regular.woff2 │ │ └── fantasque │ │ │ ├── FantasqueSansMono-Bold.woff │ │ │ ├── FantasqueSansMono-RegItalic.woff │ │ │ ├── FantasqueSansMono-Regular.woff │ │ │ └── FantasqueSansMono-BoldItalic.woff │ │ ├── lib │ │ ├── images │ │ │ ├── animated-overlay.gif │ │ │ ├── ui-icons_222222_256x240.png │ │ │ ├── ui-icons_228ef1_256x240.png │ │ │ ├── ui-icons_2e83ff_256x240.png │ │ │ ├── ui-icons_444444_256x240.png │ │ │ ├── ui-icons_454545_256x240.png │ │ │ ├── ui-icons_555555_256x240.png │ │ │ ├── ui-icons_777620_256x240.png │ │ │ ├── ui-icons_777777_256x240.png │ │ │ ├── ui-icons_888888_256x240.png │ │ │ ├── ui-icons_cc0000_256x240.png │ │ │ ├── ui-icons_cd0a0a_256x240.png │ │ │ ├── ui-icons_ef8c08_256x240.png │ │ │ ├── ui-icons_ffd27a_256x240.png │ │ │ ├── ui-icons_ffffff_256x240.png │ │ │ ├── ui-bg_flat_10_000000_40x100.png │ │ │ ├── ui-bg_glass_100_f6f6f6_1x400.png │ │ │ ├── ui-bg_glass_100_fdf5ce_1x400.png │ │ │ ├── ui-bg_glass_55_fbf9ee_1x400.png │ │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ │ ├── ui-bg_glass_75_dadada_1x400.png │ │ │ ├── ui-bg_glass_75_e6e6e6_1x400.png │ │ │ ├── ui-bg_glass_95_fef1ec_1x400.png │ │ │ ├── ui-bg_gloss-wave_35_f6a828_500x100.png │ │ │ ├── ui-bg_diagonals-thick_18_b81900_40x40.png │ │ │ ├── ui-bg_diagonals-thick_20_666666_40x40.png │ │ │ ├── ui-bg_highlight-soft_100_eeeeee_1x100.png │ │ │ ├── ui-bg_highlight-soft_75_cccccc_1x100.png │ │ │ └── ui-bg_highlight-soft_75_ffe45c_1x100.png │ │ └── highlight │ │ │ └── foundation.css │ │ └── css │ │ ├── components │ │ ├── numeric.css │ │ ├── colors.css │ │ ├── heading.css │ │ ├── summary.css │ │ ├── cell.css │ │ ├── row.css │ │ └── fonts.css │ │ └── pages │ │ ├── profiling.css │ │ ├── requests.css │ │ ├── detail_base.css │ │ ├── raw.css │ │ ├── clear_db.css │ │ ├── cprofile.css │ │ ├── profile_detail.css │ │ ├── summary.css │ │ ├── sql.css │ │ ├── sql_detail.css │ │ ├── request.css │ │ └── base.css ├── __init__.py ├── templates │ └── silk │ │ ├── inclusion │ │ ├── heading.html │ │ ├── code.html │ │ ├── profile_summary.html │ │ ├── root_menu.html │ │ ├── request_summary.html │ │ ├── request_summary_row.html │ │ ├── request_menu.html │ │ └── profile_menu.html │ │ ├── raw.html │ │ ├── base │ │ ├── detail_base.html │ │ ├── base.html │ │ └── root_base.html │ │ ├── cprofile.html │ │ ├── clear_db.html │ │ ├── sql_detail.html │ │ └── profile_detail.html ├── apps.py ├── errors.py ├── singleton.py ├── storage.py ├── auth.py ├── config.py └── urls.py ├── project ├── example_app │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_alter_blind_photo.py │ │ ├── 0003_blind_unique_name_if_provided.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── urls.py │ ├── templates │ │ └── example_app │ │ │ ├── blind_form.html │ │ │ ├── index.html │ │ │ └── login.html │ ├── models.py │ ├── views.py │ └── admin.py ├── project │ ├── __init__.py │ ├── urls.py │ └── settings.py ├── tests │ ├── data │ │ ├── __init__.py │ │ └── dynamic.py │ ├── test_lib │ │ ├── __init__.py │ │ └── assertion.py │ ├── __init__.py │ ├── urlconf_without_silk.py │ ├── test_app_config.py │ ├── test_response_assumptions.py │ ├── test_code_gen_curl.py │ ├── test_code_gen_django.py │ ├── test_compat.py │ ├── test_multipart_forms.py │ ├── test_command_garbage_collect.py │ ├── test_config_long_urls.py │ ├── util.py │ ├── factories.py │ ├── test_view_clear_db.py │ ├── test_view_summary_view.py │ ├── test_config_meta.py │ ├── test_profile_parser.py │ ├── test_code.py │ ├── test_db.py │ ├── test_config_max_body_size.py │ ├── test_config_auth.py │ ├── test_dynamic_profiling.py │ └── test_profile_dot.py ├── manage.py └── wsgi.py ├── .coveragerc ├── web.psd ├── pyproject.toml ├── docs ├── images │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ └── meta.png ├── index.rst ├── quickstart.rst ├── configuration.rst ├── troubleshooting.rst └── profiling.rst ├── screenshots ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── 9.png ├── 10.png └── meta.png ├── requirements.txt ├── pytest.ini ├── scss ├── components │ ├── numeric.scss │ ├── colors.scss │ ├── heading.scss │ ├── summary.scss │ ├── cell.scss │ ├── row.scss │ └── fonts.scss └── pages │ ├── profiling.scss │ ├── requests.scss │ ├── detail_base.scss │ ├── raw.scss │ ├── clear_db.scss │ ├── cprofile.scss │ ├── profile_detail.scss │ ├── summary.scss │ ├── sql.scss │ ├── sql_detail.scss │ ├── request.scss │ └── base.scss ├── CONTRIBUTING.md ├── MANIFEST.in ├── gulpfile.js ├── silk.sublime-project ├── package.json ├── LICENSE ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── tox.ini ├── setup.py ├── .gitignore ├── .pre-commit-config.yaml └── CODE_OF_CONDUCT.md /silk/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /silk/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project/example_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /silk/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project/example_app/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /silk/utils/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'mtford' 2 | -------------------------------------------------------------------------------- /silk/views/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'mtford' 2 | -------------------------------------------------------------------------------- /project/project/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'mtford' 2 | -------------------------------------------------------------------------------- /silk/profiling/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'mtford' 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = silk 4 | -------------------------------------------------------------------------------- /project/example_app/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /project/tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'mtford' 2 | -------------------------------------------------------------------------------- /silk/code_generation/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'mtford' 2 | -------------------------------------------------------------------------------- /silk/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'mtford' 2 | -------------------------------------------------------------------------------- /project/tests/test_lib/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'mtford' 2 | -------------------------------------------------------------------------------- /project/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * # noqa: F401, F403 2 | -------------------------------------------------------------------------------- /silk/static/silk/js/pages/raw.js: -------------------------------------------------------------------------------- 1 | hljs.initHighlightingOnLoad(); 2 | -------------------------------------------------------------------------------- /silk/static/silk/js/pages/request.js: -------------------------------------------------------------------------------- 1 | hljs.initHighlightingOnLoad(); 2 | -------------------------------------------------------------------------------- /web.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/web.psd -------------------------------------------------------------------------------- /silk/static/silk/js/pages/detail_base.js: -------------------------------------------------------------------------------- 1 | hljs.initHighlightingOnLoad(); 2 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.autopep8] 2 | ignore = "E501,E203,W503" 3 | in-place = true 4 | -------------------------------------------------------------------------------- /docs/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/docs/images/1.png -------------------------------------------------------------------------------- /docs/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/docs/images/2.png -------------------------------------------------------------------------------- /docs/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/docs/images/3.png -------------------------------------------------------------------------------- /docs/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/docs/images/4.png -------------------------------------------------------------------------------- /docs/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/docs/images/5.png -------------------------------------------------------------------------------- /docs/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/docs/images/6.png -------------------------------------------------------------------------------- /docs/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/docs/images/7.png -------------------------------------------------------------------------------- /docs/images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/docs/images/8.png -------------------------------------------------------------------------------- /docs/images/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/docs/images/9.png -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/screenshots/4.png -------------------------------------------------------------------------------- /screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/screenshots/5.png -------------------------------------------------------------------------------- /screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/screenshots/6.png -------------------------------------------------------------------------------- /screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/screenshots/7.png -------------------------------------------------------------------------------- /screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/screenshots/8.png -------------------------------------------------------------------------------- /screenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/screenshots/9.png -------------------------------------------------------------------------------- /docs/images/meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/docs/images/meta.png -------------------------------------------------------------------------------- /screenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/screenshots/10.png -------------------------------------------------------------------------------- /screenshots/meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/screenshots/meta.png -------------------------------------------------------------------------------- /silk/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version 2 | 3 | __version__ = version("django-silk") 4 | -------------------------------------------------------------------------------- /silk/static/silk/filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/filter.png -------------------------------------------------------------------------------- /silk/static/silk/filter2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/filter2.png -------------------------------------------------------------------------------- /silk/static/silk/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/favicon-16x16.png -------------------------------------------------------------------------------- /silk/static/silk/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/favicon-32x32.png -------------------------------------------------------------------------------- /silk/static/silk/js/pages/profiling.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | initFilters(); 3 | initFilterButton(); 4 | }); 5 | -------------------------------------------------------------------------------- /silk/static/silk/js/pages/requests.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | initFilters(); 3 | initFilterButton(); 4 | }); 5 | -------------------------------------------------------------------------------- /silk/static/silk/fonts/fira/FiraSans-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/fira/FiraSans-Bold.woff -------------------------------------------------------------------------------- /silk/static/silk/fonts/fira/FiraSans-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/fira/FiraSans-Light.woff -------------------------------------------------------------------------------- /silk/templates/silk/inclusion/heading.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ text }} 4 |
5 |
6 | -------------------------------------------------------------------------------- /silk/static/silk/fonts/fira/FiraSans-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/fira/FiraSans-Medium.woff -------------------------------------------------------------------------------- /silk/static/silk/fonts/fira/FiraSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/fira/FiraSans-Regular.woff -------------------------------------------------------------------------------- /silk/static/silk/lib/images/animated-overlay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/animated-overlay.gif -------------------------------------------------------------------------------- /silk/static/silk/fonts/fira/FiraSans-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/fira/FiraSans-BoldItalic.woff -------------------------------------------------------------------------------- /silk/static/silk/fonts/fira/FiraSans-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/fira/FiraSans-LightItalic.woff -------------------------------------------------------------------------------- /silk/static/silk/fonts/fira/FiraSans-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/fira/FiraSans-MediumItalic.woff -------------------------------------------------------------------------------- /silk/static/silk/fonts/fira/FiraSans-RegularItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/fira/FiraSans-RegularItalic.woff -------------------------------------------------------------------------------- /silk/static/silk/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /silk/static/silk/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_228ef1_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_228ef1_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_2e83ff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_2e83ff_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_444444_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_444444_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_555555_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_555555_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_777620_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_777620_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_777777_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_777777_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_888888_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_888888_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_cc0000_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_cc0000_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_cd0a0a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_cd0a0a_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_ef8c08_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_ef8c08_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_ffd27a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_ffd27a_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /silk/static/silk/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /silk/static/silk/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /silk/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SilkAppConfig(AppConfig): 5 | default_auto_field = "django.db.models.AutoField" 6 | name = "silk" 7 | -------------------------------------------------------------------------------- /silk/static/silk/fonts/fantasque/FantasqueSansMono-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/fantasque/FantasqueSansMono-Bold.woff -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_flat_10_000000_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_flat_10_000000_40x100.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_glass_100_f6f6f6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_glass_100_f6f6f6_1x400.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_glass_100_fdf5ce_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_glass_100_fdf5ce_1x400.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_glass_55_fbf9ee_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_glass_55_fbf9ee_1x400.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_glass_75_dadada_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_glass_75_dadada_1x400.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_glass_75_e6e6e6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_glass_75_e6e6e6_1x400.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_glass_95_fef1ec_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_glass_95_fef1ec_1x400.png -------------------------------------------------------------------------------- /silk/static/silk/fonts/fantasque/FantasqueSansMono-RegItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/fantasque/FantasqueSansMono-RegItalic.woff -------------------------------------------------------------------------------- /silk/static/silk/fonts/fantasque/FantasqueSansMono-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/fantasque/FantasqueSansMono-Regular.woff -------------------------------------------------------------------------------- /silk/static/silk/fonts/fantasque/FantasqueSansMono-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/fonts/fantasque/FantasqueSansMono-BoldItalic.woff -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_gloss-wave_35_f6a828_500x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_gloss-wave_35_f6a828_500x100.png -------------------------------------------------------------------------------- /project/tests/data/dynamic.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | print('1') 3 | print('2') 4 | print('3') 5 | 6 | 7 | def foo2(): 8 | print('1') 9 | print('2') 10 | print('3') 11 | -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_diagonals-thick_18_b81900_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_diagonals-thick_18_b81900_40x40.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_diagonals-thick_20_666666_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_diagonals-thick_20_666666_40x40.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_highlight-soft_100_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_highlight-soft_100_eeeeee_1x100.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_highlight-soft_75_cccccc_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_highlight-soft_75_cccccc_1x100.png -------------------------------------------------------------------------------- /silk/static/silk/lib/images/ui-bg_highlight-soft_75_ffe45c_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-silk/master/silk/static/silk/lib/images/ui-bg_highlight-soft_75_ffe45c_1x100.png -------------------------------------------------------------------------------- /silk/errors.py: -------------------------------------------------------------------------------- 1 | class SilkError(Exception): 2 | pass 3 | 4 | 5 | class SilkNotConfigured(SilkError): 6 | pass 7 | 8 | 9 | class SilkInternalInconsistency(SilkError): 10 | pass 11 | -------------------------------------------------------------------------------- /silk/templates/silk/inclusion/code.html: -------------------------------------------------------------------------------- 1 |
...
2 | {% for line in code %}{{ line }}
{% endfor %}...
3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | coverage==7.13.0 2 | factory-boy==3.3.3 3 | freezegun==1.5.5 4 | networkx==3.4.2 5 | pillow==12.0.0 6 | pydot==3.0.4 7 | pygments==2.19.2 8 | pytest-cov==7.0.0 9 | pytest-django==4.11.1 10 | -------------------------------------------------------------------------------- /project/tests/urlconf_without_silk.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | urlpatterns = [ 4 | path( 5 | 'example_app/', 6 | include('example_app.urls', namespace='example_app') 7 | ), 8 | ] 9 | -------------------------------------------------------------------------------- /silk/static/silk/js/pages/summary.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | initFilters(); 3 | var $inputs = $('.resizing-input'); 4 | $inputs.focusout(function () { 5 | $('#filter-form').submit(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /silk/static/silk/js/pages/clear_db.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | initFilters(); 3 | var $inputs = $('.resizing-input'); 4 | $inputs.focusout(function () { 5 | $('#filter-form').submit(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /silk/static/silk/js/pages/sql_detail.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | configureSpanFontColors($('#num-joins-div').find('.numeric'), 3, 5); 3 | configureSpanFontColors($('#time-taken-div').find('.numeric'), 200, 500); 4 | }); 5 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --cov silk --cov-config .coveragerc --cov-append --cov-report term --cov-report=xml 3 | python_files = test.py tests.py test_*.py tests_*.py *_tests.py *_test.py 4 | DJANGO_SETTINGS_MODULE = project.settings 5 | -------------------------------------------------------------------------------- /scss/components/numeric.scss: -------------------------------------------------------------------------------- 1 | .numeric { 2 | font-weight: normal; 3 | } 4 | 5 | .unit { 6 | font-weight: normal; 7 | } 8 | 9 | .numeric .unit { 10 | font-size: 12px; 11 | } 12 | 13 | .numeric { 14 | font-size: 20px; 15 | } 16 | -------------------------------------------------------------------------------- /silk/static/silk/css/components/numeric.css: -------------------------------------------------------------------------------- 1 | .numeric { 2 | font-weight: normal; 3 | } 4 | 5 | .unit { 6 | font-weight: normal; 7 | } 8 | 9 | .numeric .unit { 10 | font-size: 12px; 11 | } 12 | 13 | .numeric { 14 | font-size: 20px; 15 | } 16 | -------------------------------------------------------------------------------- /silk/static/silk/css/pages/profiling.css: -------------------------------------------------------------------------------- 1 | .name-div { 2 | font-weight: bold; 3 | } 4 | 5 | .container { 6 | padding: 0 1em; 7 | } 8 | 9 | h2 { 10 | margin-bottom: 10px; 11 | } 12 | 13 | .pyprofile { 14 | overflow: scroll; 15 | max-height: 650px; 16 | } 17 | -------------------------------------------------------------------------------- /project/tests/test_lib/assertion.py: -------------------------------------------------------------------------------- 1 | def dict_contains(child_dict, parent_dict): 2 | for key, value in child_dict.items(): 3 | if key not in parent_dict: 4 | return False 5 | if parent_dict[key] != value: 6 | return False 7 | return True 8 | -------------------------------------------------------------------------------- /project/example_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'example_app' 6 | urlpatterns = [ 7 | path(route='', view=views.index, name='index'), 8 | path(route='create', view=views.ExampleCreateView.as_view(), name='create'), 9 | ] 10 | -------------------------------------------------------------------------------- /scss/pages/profiling.scss: -------------------------------------------------------------------------------- 1 | .name-div { 2 | font-weight: bold; 3 | } 4 | 5 | .container { 6 | padding: 0 1em; 7 | } 8 | 9 | .description { 10 | 11 | } 12 | 13 | h2 { 14 | margin-bottom: 10px; 15 | } 16 | 17 | .pyprofile { 18 | overflow: scroll; 19 | max-height: 650px; 20 | } 21 | -------------------------------------------------------------------------------- /silk/static/silk/js/pages/base.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | configureSpanFontColors($('#num-joins-div').find('.numeric'), 3, 5); 3 | configureSpanFontColors($('#time-taken-div').find('.numeric'), 200, 500); 4 | configureSpanFontColors($('#num-queries-div').find('.numeric'), 10, 500); 5 | }); 6 | -------------------------------------------------------------------------------- /scss/components/colors.scss: -------------------------------------------------------------------------------- 1 | .very-good-font-color { 2 | color: #bac54b; 3 | } 4 | 5 | .good-font-color { 6 | color: #c3a948; 7 | } 8 | 9 | .ok-font-color { 10 | color: #c08245; 11 | } 12 | 13 | .bad-font-color { 14 | color: #be5b43; 15 | } 16 | 17 | .very-bad-font-color { 18 | color: #b9424f; 19 | } 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) 2 | 3 | This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README* 3 | recursive-include silk/templates * 4 | recursive-include silk/static * 5 | recursive-include silk/code_generation *.py 6 | recursive-include silk/profiling *.py 7 | recursive-include silk/utils *.py 8 | recursive-include silk/views *.py 9 | recursive-include silk *.py 10 | -------------------------------------------------------------------------------- /silk/static/silk/css/components/colors.css: -------------------------------------------------------------------------------- 1 | .very-good-font-color { 2 | color: #bac54b; 3 | } 4 | 5 | .good-font-color { 6 | color: #c3a948; 7 | } 8 | 9 | .ok-font-color { 10 | color: #c08245; 11 | } 12 | 13 | .bad-font-color { 14 | color: #be5b43; 15 | } 16 | 17 | .very-bad-font-color { 18 | color: #b9424f; 19 | } 20 | -------------------------------------------------------------------------------- /scss/components/heading.scss: -------------------------------------------------------------------------------- 1 | .heading { 2 | width: 100%; 3 | background-color: transparent; 4 | height: 30px; 5 | display: table; 6 | font-weight: bold; 7 | margin-top: 20px; 8 | .inner-heading { 9 | display: table-cell; 10 | text-align: left; 11 | padding: 0; 12 | vertical-align: middle; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /silk/static/silk/css/components/heading.css: -------------------------------------------------------------------------------- 1 | .heading { 2 | width: 100%; 3 | background-color: transparent; 4 | height: 30px; 5 | display: table; 6 | font-weight: bold; 7 | margin-top: 20px; 8 | } 9 | .heading .inner-heading { 10 | display: table-cell; 11 | text-align: left; 12 | padding: 0; 13 | vertical-align: middle; 14 | } 15 | -------------------------------------------------------------------------------- /project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Define the Django Silk management entry.""" 3 | import os 4 | import sys 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /project/example_app/templates/example_app/blind_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Example App

4 |

Use this app for testing and playing around with Silk. Displays a Blind creation form.

5 |
6 | {% csrf_token %} 7 | {{ form.as_p }} 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /scss/components/summary.scss: -------------------------------------------------------------------------------- 1 | #error-div { 2 | margin: 10px; 3 | } 4 | 5 | #query-div { 6 | margin: auto; 7 | width: 960px; 8 | text-align: center; 9 | } 10 | 11 | #code { 12 | text-align: left; 13 | } 14 | 15 | .name-div { 16 | margin-top: 20px; 17 | margin-bottom: 15px; 18 | font-weight: bold; 19 | } 20 | 21 | .description { 22 | text-align: left; 23 | } 24 | -------------------------------------------------------------------------------- /scss/pages/requests.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 1em; 3 | } 4 | 5 | .resizing-input input { 6 | background-color: white; 7 | padding-top: 2px; 8 | color: black; 9 | box-shadow: inset 0 0 3px black; 10 | } 11 | 12 | .resizing-input input::placeholder { 13 | color: #383838; 14 | opacity: 1; 15 | } 16 | 17 | .filter-section { 18 | line-height: 2.3; 19 | } 20 | -------------------------------------------------------------------------------- /silk/singleton.py: -------------------------------------------------------------------------------- 1 | __author__ = 'mtford' 2 | 3 | 4 | class Singleton(type, metaclass=object): 5 | def __init__(cls, name, bases, d): 6 | super().__init__(name, bases, d) 7 | cls.instance = None 8 | 9 | def __call__(cls, *args): 10 | if cls.instance is None: 11 | cls.instance = super().__call__(*args) 12 | return cls.instance 13 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | let gulp = require('gulp'), 2 | sass = require('gulp-sass'); 3 | 4 | 5 | gulp.task('watch', function () { 6 | gulp.watch('scss/**/*.scss', gulp.series('sass')); 7 | }); 8 | 9 | gulp.task('sass', function () { 10 | return gulp.src('scss/**/*.scss') 11 | .pipe(sass().on('error', sass.logError)) 12 | .pipe(gulp.dest('silk/static/silk/css')); 13 | }); 14 | -------------------------------------------------------------------------------- /silk/static/silk/css/components/summary.css: -------------------------------------------------------------------------------- 1 | #error-div { 2 | margin: 10px; 3 | } 4 | 5 | #query-div { 6 | margin: auto; 7 | width: 960px; 8 | text-align: center; 9 | } 10 | 11 | #code { 12 | text-align: left; 13 | } 14 | 15 | .name-div { 16 | margin-top: 20px; 17 | margin-bottom: 15px; 18 | font-weight: bold; 19 | } 20 | 21 | .description { 22 | text-align: left; 23 | } 24 | -------------------------------------------------------------------------------- /silk/static/silk/css/pages/requests.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 1em; 3 | } 4 | 5 | .resizing-input input { 6 | background-color: white; 7 | padding-top: 2px; 8 | color: black; 9 | box-shadow: inset 0 0 3px black; 10 | } 11 | 12 | .resizing-input input::placeholder { 13 | color: #383838; 14 | opacity: 1; 15 | } 16 | 17 | .filter-section { 18 | line-height: 2.3; 19 | } 20 | -------------------------------------------------------------------------------- /silk.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [{ 3 | "follow_symlinks": true, 4 | "path": ".", 5 | "folder_exclude_patterns": [ 6 | ".idea", 7 | "bower_components", 8 | "node_modules" 9 | ], 10 | "file_exclude_patterns": [ 11 | ".gitmodules", 12 | ".gitignore", 13 | "LICENSE" 14 | ] 15 | }] 16 | } 17 | -------------------------------------------------------------------------------- /project/tests/test_app_config.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps as proj_apps 2 | from django.test import TestCase 3 | 4 | from silk.apps import SilkAppConfig 5 | 6 | 7 | class TestAppConfig(TestCase): 8 | """ 9 | Test if correct AppConfig class is loaded by Django. 10 | """ 11 | 12 | def test_app_config_loaded(self): 13 | silk_app_config = proj_apps.get_app_config("silk") 14 | self.assertIsInstance(silk_app_config, SilkAppConfig) 15 | -------------------------------------------------------------------------------- /project/wsgi.py: -------------------------------------------------------------------------------- 1 | """WSGI config for django_silky project. 2 | 3 | It exposes the WSGI callable as a module-level variable named ``application``. 4 | 5 | For more information on this file, see 6 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 7 | 8 | """ 9 | import os 10 | 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 12 | 13 | from django.core.wsgi import get_wsgi_application # noqa: E402 14 | 15 | application = get_wsgi_application() 16 | -------------------------------------------------------------------------------- /silk/storage.py: -------------------------------------------------------------------------------- 1 | from django.core.files.storage import FileSystemStorage 2 | 3 | from silk.config import SilkyConfig 4 | 5 | 6 | class ProfilerResultStorage(FileSystemStorage): 7 | # the default storage will only store under MEDIA_ROOT, so we must define our own. 8 | def __init__(self): 9 | super().__init__( 10 | location=SilkyConfig().SILKY_PYTHON_PROFILER_RESULT_PATH, 11 | base_url='' 12 | ) 13 | self.base_url = None 14 | -------------------------------------------------------------------------------- /silk/templatetags/silk_nav.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.urls import reverse 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.simple_tag 8 | def navactive(request, urls, *args, **kwargs): 9 | path = request.path 10 | urls = [reverse(url, args=args) for url in urls.split()] 11 | if path in urls: 12 | cls = kwargs.get('class', None) 13 | if not cls: 14 | cls = "menu-item-selected" 15 | return cls 16 | return "" 17 | -------------------------------------------------------------------------------- /project/tests/test_response_assumptions.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.test import TestCase 3 | 4 | 5 | class TestResponseAssumptions(TestCase): 6 | 7 | def test_headers_present_in_http_response(self): 8 | """Verify that HttpResponse has a headers or _headers attribute, which we use and Mock in our tests.""" 9 | django_response = HttpResponse() 10 | self.assertTrue(hasattr(django_response, '_headers') or hasattr(django_response, 'headers')) 11 | -------------------------------------------------------------------------------- /silk/migrations/0008_sqlquery_analysis.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2020-11-26 13:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('silk', '0007_sqlquery_identifier'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='sqlquery', 15 | name='analysis', 16 | field=models.TextField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /project/example_app/migrations/0002_alter_blind_photo.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-12 22:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('example_app', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='blind', 15 | name='photo', 16 | field=models.ImageField(upload_to='products'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /silk/migrations/0007_sqlquery_identifier.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2019-10-26 12:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('silk', '0006_fix_request_prof_file_blank'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='sqlquery', 15 | name='identifier', 16 | field=models.IntegerField(default=-1), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /scss/pages/detail_base.scss: -------------------------------------------------------------------------------- 1 | #traceback { 2 | overflow: visible; 3 | } 4 | 5 | #time-div { 6 | text-align: center; 7 | margin-bottom: 30px; 8 | } 9 | 10 | #query-div { 11 | text-align: center; 12 | margin-bottom: 20px; 13 | } 14 | 15 | #query { 16 | text-align: left; 17 | margin: 0 auto; 18 | display: inline-block; 19 | } 20 | 21 | .line { 22 | width: 100%; 23 | display: inline-block; 24 | } 25 | 26 | .the-line { 27 | background-color: #c3c3c3; 28 | } 29 | 30 | pre { 31 | margin: 0; 32 | } 33 | -------------------------------------------------------------------------------- /silk/migrations/0003_request_prof_file.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.7 on 2016-07-08 18:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('silk', '0002_auto_update_uuid4_id_field'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='request', 15 | name='prof_file', 16 | field=models.FileField(null=True, upload_to=''), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /silk/utils/pagination.py: -------------------------------------------------------------------------------- 1 | from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator 2 | 3 | __author__ = 'mtford' 4 | 5 | 6 | def _page(request, query_set, per_page=200): 7 | paginator = Paginator(query_set, per_page) 8 | page_number = request.GET.get('page') 9 | try: 10 | page = paginator.page(page_number) 11 | except PageNotAnInteger: 12 | page = paginator.page(1) 13 | except EmptyPage: 14 | page = paginator.page(paginator.num_pages) 15 | return page 16 | -------------------------------------------------------------------------------- /silk/static/silk/css/pages/detail_base.css: -------------------------------------------------------------------------------- 1 | #traceback { 2 | overflow: visible; 3 | } 4 | 5 | #time-div { 6 | text-align: center; 7 | margin-bottom: 30px; 8 | } 9 | 10 | #query-div { 11 | text-align: center; 12 | margin-bottom: 20px; 13 | } 14 | 15 | #query { 16 | text-align: left; 17 | margin: 0 auto; 18 | display: inline-block; 19 | } 20 | 21 | .line { 22 | width: 100%; 23 | display: inline-block; 24 | } 25 | 26 | .the-line { 27 | background-color: #c3c3c3; 28 | } 29 | 30 | pre { 31 | margin: 0; 32 | } 33 | -------------------------------------------------------------------------------- /silk/static/silk/js/pages/root_base.js: -------------------------------------------------------------------------------- 1 | function initFilterButton() { 2 | $('#filter-button').click(function () { 3 | $(this).toggleClass('active'); 4 | $('body').toggleClass('cbp-spmenu-push-toleft'); 5 | $('#cbp-spmenu-s2').toggleClass('cbp-spmenu-open'); 6 | initFilters(); 7 | }); 8 | } 9 | function submitFilters() { 10 | $('#filter-form2').submit(); 11 | } 12 | function submitEmptyFilters() { 13 | $('#cbp-spmenu-s2 :input:not([type=hidden])').val(''); 14 | submitFilters(); 15 | } 16 | -------------------------------------------------------------------------------- /silk/static/silk/js/pages/sql.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | document.querySelectorAll(".data-row").forEach((rowElement) => { 3 | let sqlDetailUrl = rowElement.dataset.sqlDetailUrl; 4 | rowElement.addEventListener("click", (e) => { 5 | switch (e.button) { 6 | case 0: 7 | window.location = sqlDetailUrl; 8 | break; 9 | case 1: 10 | window.open(sqlDetailUrl); 11 | break; 12 | default: 13 | break; 14 | } 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /silk/static/silk/js/pages/profile_detail.js: -------------------------------------------------------------------------------- 1 | function createViz() { 2 | var profileDotURL = JSON.parse(document.getElementById("profileDotURL").textContent); 3 | 4 | $.get( 5 | profileDotURL, 6 | { cutoff: $('#percent').val() }, 7 | function (response) { 8 | var svg = '#graph-div'; 9 | $(svg).html(Viz(response.dot)); 10 | $(svg + ' svg').attr('width', 960).attr('height', 600); 11 | svgPanZoom(svg + ' svg', { controlIconsEnabled: true }); 12 | } 13 | ); 14 | } 15 | createViz(); 16 | -------------------------------------------------------------------------------- /silk/templates/silk/raw.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
{{ body }}
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /silk/migrations/0004_request_prof_file_storage.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.4 on 2016-12-06 00:23 2 | 3 | from django.db import migrations, models 4 | 5 | import silk.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('silk', '0003_request_prof_file'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='request', 17 | name='prof_file', 18 | field=models.FileField(null=True, storage=silk.models.silk_storage, upload_to=''), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /project/example_app/migrations/0003_blind_unique_name_if_provided.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2022-10-28 08:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('example_app', '0002_alter_blind_photo'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddConstraint( 14 | model_name='blind', 15 | constraint=models.UniqueConstraint(condition=models.Q(('name', ''), _negated=True), fields=('name',), name='unique_name_if_provided'), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /silk/migrations/0005_increase_request_prof_file_length.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.3 on 2017-07-31 23:40 2 | 3 | from django.db import migrations, models 4 | 5 | import silk.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('silk', '0004_request_prof_file_storage'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='request', 17 | name='prof_file', 18 | field=models.FileField(max_length=300, null=True, storage=silk.models.silk_storage, upload_to=''), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /silk/migrations/0006_fix_request_prof_file_blank.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2017-12-28 14:21 2 | 3 | from django.db import migrations, models 4 | 5 | import silk.storage 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('silk', '0005_increase_request_prof_file_length'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='request', 17 | name='prof_file', 18 | field=models.FileField(blank=True, max_length=300, storage=silk.storage.ProfilerResultStorage(), upload_to=''), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /silk/management/commands/silk_clear_request_log.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | import silk.models 4 | from silk.utils.data_deletion import delete_model 5 | 6 | 7 | class Command(BaseCommand): 8 | help = "Clears silk's log of requests." 9 | 10 | def handle(self, *args, **options): 11 | # Django takes a long time to traverse foreign key relations, 12 | # so delete in the order that makes it easy. 13 | delete_model(silk.models.Profile) 14 | delete_model(silk.models.SQLQuery) 15 | delete_model(silk.models.Response) 16 | delete_model(silk.models.Request) 17 | -------------------------------------------------------------------------------- /silk/utils/profile_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | _pattern = re.compile(' +') 4 | 5 | 6 | def parse_profile(output): 7 | """ 8 | Parse the output of cProfile to a list of tuples. 9 | """ 10 | if isinstance(output, str): 11 | output = output.split('\n') 12 | for i, line in enumerate(output): 13 | # ignore n function calls, total time and ordered by and empty lines 14 | line = line.strip() 15 | if i > 3 and line: 16 | columns = _pattern.split(line)[0:] 17 | function = ' '.join(columns[5:]) 18 | columns = columns[:5] + [function] 19 | yield columns 20 | -------------------------------------------------------------------------------- /silk/templatetags/silk_urls.py: -------------------------------------------------------------------------------- 1 | from django.template import Library 2 | from django.urls import reverse 3 | 4 | register = Library() 5 | 6 | 7 | @register.simple_tag 8 | def sql_detail_url(silk_request, profile, sql_query): 9 | if profile and silk_request: 10 | return reverse( 11 | "silk:request_and_profile_sql_detail", 12 | args=[silk_request.id, profile.id, sql_query.id], 13 | ) 14 | elif profile: 15 | return reverse("silk:profile_sql_detail", args=[profile.id, sql_query.id]) 16 | elif silk_request: 17 | return reverse("silk:request_sql_detail", args=[silk_request.id, sql_query.id]) 18 | -------------------------------------------------------------------------------- /silk/static/silk/css/components/cell.css: -------------------------------------------------------------------------------- 1 | .cell { 2 | display: inline-block; 3 | background-color: transparent; 4 | padding: 10px; 5 | margin-left: 10px; 6 | margin-top: 10px; 7 | border-radius: 4px; 8 | transition: background-color 0.15s ease, color 0.15s ease; 9 | } 10 | .cell div { 11 | margin: 2px; 12 | } 13 | .cell .timestamp-div { 14 | margin-bottom: 15px; 15 | font-size: 13px; 16 | } 17 | .cell .meta { 18 | font-size: 12px; 19 | color: #be5b43; 20 | } 21 | .cell .meta .unit { 22 | font-size: 9px; 23 | font-weight: lighter !important; 24 | } 25 | .cell .method-div { 26 | font-weight: bold; 27 | font-size: 20px; 28 | } 29 | .cell .path-div { 30 | font-size: 18px; 31 | margin-bottom: 15px; 32 | } 33 | -------------------------------------------------------------------------------- /scss/components/cell.scss: -------------------------------------------------------------------------------- 1 | .cell { 2 | display: inline-block; 3 | background-color: transparent; 4 | padding: 10px; 5 | margin-left: 10px; 6 | margin-top: 10px; 7 | border-radius: 4px; 8 | transition: background-color 0.15s ease, color 0.15s ease; 9 | div { 10 | margin: 2px; 11 | } 12 | .timestamp-div { 13 | margin-bottom: 15px; 14 | font-size: 13px; 15 | } 16 | .meta { 17 | font-size: 12px; 18 | color: #be5b43; 19 | .unit { 20 | font-size: 9px; 21 | font-weight: lighter !important; 22 | } 23 | 24 | } 25 | .method-div { 26 | font-weight: bold; 27 | font-size: 20px; 28 | } 29 | .path-div { 30 | font-size: 18px; 31 | margin-bottom: 15px; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /scss/pages/raw.scss: -------------------------------------------------------------------------------- 1 | pre { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | background-color: white !important; 7 | white-space: pre-wrap; /* css-3 */ 8 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 9 | /*noinspection CssInvalidElement*/ 10 | white-space: -pre-wrap; /* Opera 4-6 */ 11 | white-space: -o-pre-wrap; /* Opera 7 */ 12 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 13 | } 14 | 15 | code { 16 | font-family: Fantasque; 17 | background-color: white !important; 18 | width: 100% !important; 19 | height: auto; 20 | padding:0 !important; 21 | } 22 | 23 | body { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | html { 29 | margin: 0; 30 | padding: 0; 31 | } 32 | -------------------------------------------------------------------------------- /silk/migrations/0002_auto_update_uuid4_id_field.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('silk', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='request', 15 | name='id', 16 | field=models.CharField(default=uuid.uuid4, max_length=36, serialize=False, primary_key=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='response', 20 | name='id', 21 | field=models.CharField(default=uuid.uuid4, max_length=36, serialize=False, primary_key=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /project/tests/test_code_gen_curl.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | from unittest import TestCase 3 | 4 | from silk.code_generation.curl import curl_cmd 5 | 6 | 7 | class TestCodeGenCurl(TestCase): 8 | def test_post_json(self): 9 | result = curl_cmd( 10 | url="https://example.org/alpha/beta", 11 | method="POST", 12 | body={"gamma": "delta"}, 13 | content_type="application/json", 14 | ) 15 | 16 | result_words = shlex.split(result) 17 | 18 | self.assertEqual(result_words, [ 19 | 'curl', '-X', 'POST', 20 | '-H', 'content-type: application/json', 21 | '-d', '{"gamma": "delta"}', 22 | 'https://example.org/alpha/beta' 23 | ]) 24 | -------------------------------------------------------------------------------- /silk/static/silk/css/pages/raw.css: -------------------------------------------------------------------------------- 1 | pre { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | background-color: white !important; 7 | white-space: pre-wrap; /* css-3 */ 8 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 9 | /*noinspection CssInvalidElement*/ 10 | white-space: -pre-wrap; /* Opera 4-6 */ 11 | white-space: -o-pre-wrap; /* Opera 7 */ 12 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 13 | } 14 | 15 | code { 16 | font-family: Fantasque; 17 | background-color: white !important; 18 | width: 100% !important; 19 | height: auto; 20 | padding: 0 !important; 21 | } 22 | 23 | body { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | html { 29 | margin: 0; 30 | padding: 0; 31 | } 32 | -------------------------------------------------------------------------------- /project/example_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | from django.db.models import BooleanField, ImageField, TextField 5 | 6 | 7 | class Product(models.Model): 8 | photo = ImageField(upload_to='products') 9 | 10 | class Meta: 11 | abstract = True 12 | 13 | 14 | class Blind(Product): 15 | name = TextField() 16 | child_safe = BooleanField(default=False) 17 | 18 | def __str__(self): 19 | return self.name 20 | 21 | class Meta: 22 | constraints = [ 23 | models.UniqueConstraint( 24 | fields=["name"], 25 | condition=~models.Q(name=""), 26 | name="unique_name_if_provided", 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "silk", 3 | "version": "5.4.3", 4 | "description": "https://github.com/jazzband/django-silk", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs", 8 | "test": "tests" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/jazzband/django-silk.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/jazzband/django-silk/issues" 21 | }, 22 | "homepage": "https://github.com/jazzband/django-silk", 23 | "devDependencies": { 24 | "gulp": "^4.0.2", 25 | "gulp-sass": "^4.0.2" 26 | }, 27 | "dependencies": {} 28 | } 29 | -------------------------------------------------------------------------------- /scss/pages/clear_db.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | margin-bottom: 20px; 4 | } 5 | 6 | .inner { 7 | margin: auto; 8 | width: 960px; 9 | } 10 | 11 | .cleardb-form .cleardb-form-wrapper{ 12 | margin-bottom: 20px; 13 | } 14 | 15 | .cleardb-form label { 16 | display: block; 17 | margin-bottom: 8px; 18 | } 19 | 20 | .cleardb-form .btn { 21 | background: #333344; 22 | color: #fff; 23 | padding: 10px 20px; 24 | border-radius: 2px; 25 | cursor: pointer; 26 | box-shadow: none; 27 | font-size: 16px; 28 | line-height: 20px; 29 | border: 0; 30 | min-width: 150px; 31 | text-align: center; 32 | } 33 | .cleardb-form label :last-child { 34 | margin-bottom: 0; 35 | } 36 | .msg { 37 | margin-top: 20px; 38 | color: #bac54b; 39 | } 40 | -------------------------------------------------------------------------------- /silk/views/cprofile.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.utils.decorators import method_decorator 3 | from django.views.generic import View 4 | 5 | from silk.auth import login_possibly_required, permissions_possibly_required 6 | from silk.models import Request 7 | 8 | 9 | class CProfileView(View): 10 | 11 | @method_decorator(login_possibly_required) 12 | @method_decorator(permissions_possibly_required) 13 | def get(self, request, *_, **kwargs): 14 | request_id = kwargs['request_id'] 15 | silk_request = Request.objects.get(pk=request_id) 16 | context = { 17 | 'silk_request': silk_request, 18 | 'request': request} 19 | 20 | return render(request, 'silk/cprofile.html', context) 21 | -------------------------------------------------------------------------------- /project/example_app/views.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | # Create your views here. 4 | from django.shortcuts import render 5 | from django.urls import reverse_lazy 6 | from django.views.generic import CreateView 7 | from example_app import models 8 | 9 | from silk.profiling.profiler import silk_profile 10 | 11 | 12 | def index(request): 13 | @silk_profile() 14 | def do_something_long(): 15 | sleep(1.345) 16 | 17 | with silk_profile(name='Why do this take so long?'): 18 | do_something_long() 19 | return render(request, 'example_app/index.html', {'blinds': models.Blind.objects.all()}) 20 | 21 | 22 | class ExampleCreateView(CreateView): 23 | model = models.Blind 24 | fields = ['name'] 25 | success_url = reverse_lazy('example_app:index') 26 | -------------------------------------------------------------------------------- /silk/static/silk/css/pages/clear_db.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | margin-bottom: 20px; 4 | } 5 | 6 | .inner { 7 | margin: auto; 8 | width: 960px; 9 | } 10 | 11 | .cleardb-form .cleardb-form-wrapper { 12 | margin-bottom: 20px; 13 | } 14 | 15 | .cleardb-form label { 16 | display: block; 17 | margin-bottom: 8px; 18 | } 19 | 20 | .cleardb-form .btn { 21 | background: #333344; 22 | color: #fff; 23 | padding: 10px 20px; 24 | border-radius: 2px; 25 | cursor: pointer; 26 | box-shadow: none; 27 | font-size: 16px; 28 | line-height: 20px; 29 | border: 0; 30 | min-width: 150px; 31 | text-align: center; 32 | } 33 | 34 | .cleardb-form label :last-child { 35 | margin-bottom: 0; 36 | } 37 | 38 | .msg { 39 | margin-top: 20px; 40 | color: #bac54b; 41 | } 42 | -------------------------------------------------------------------------------- /project/example_app/templates/example_app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 |

Example App

11 |

Use this app for testing and playing around with Silk. Displays a range of Blinds. Use admin to add them.

12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for blind in blinds %} 19 | 20 | 21 | 22 | 23 | 24 | {% endfor %} 25 |
PhotoNameChild safe?
{% if blind.photo %}{% endif %}{{ blind.name }}{% if blind.child_safe %}Yes{% else %}No{% endif %}
26 | 27 | 28 | -------------------------------------------------------------------------------- /scss/pages/cprofile.scss: -------------------------------------------------------------------------------- 1 | #query-info-div { 2 | margin-top: 15px; 3 | } 4 | 5 | #query-info-div .timestamp-div { 6 | font-size: 13px; 7 | 8 | } 9 | 10 | #pyprofile-div { 11 | display: block; 12 | margin: auto; 13 | width: 960px; 14 | } 15 | 16 | .pyprofile { 17 | text-align: left; 18 | } 19 | 20 | a { 21 | color: #45ADA8; 22 | } 23 | 24 | a:visited { 25 | color: #45ADA8; 26 | } 27 | 28 | a:hover { 29 | color: #547980; 30 | } 31 | 32 | a:active { 33 | color: #594F4F; 34 | } 35 | 36 | #graph-div { 37 | padding: 25px; 38 | background-color: white; 39 | display: block; 40 | margin-left: auto; 41 | margin-right: auto; 42 | margin-top: 25px; 43 | width: 960px; 44 | text-align: center; 45 | } 46 | 47 | #percent { 48 | width: 20px; 49 | } 50 | 51 | svg { 52 | display: block; 53 | } 54 | -------------------------------------------------------------------------------- /silk/views/profile_download.py: -------------------------------------------------------------------------------- 1 | from django.http import FileResponse 2 | from django.shortcuts import get_object_or_404 3 | from django.utils.decorators import method_decorator 4 | from django.views.generic import View 5 | 6 | from silk.auth import login_possibly_required, permissions_possibly_required 7 | from silk.models import Request 8 | 9 | 10 | class ProfileDownloadView(View): 11 | 12 | @method_decorator(login_possibly_required) 13 | @method_decorator(permissions_possibly_required) 14 | def get(self, request, request_id): 15 | silk_request = get_object_or_404(Request, pk=request_id, prof_file__isnull=False) 16 | response = FileResponse(silk_request.prof_file) 17 | response['Content-Disposition'] = f'attachment; filename="{silk_request.prof_file.name}"' 18 | return response 19 | -------------------------------------------------------------------------------- /project/example_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.9.7 on 2016-07-08 13:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Blind', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('photo', models.ImageField(upload_to=b'products')), 19 | ('name', models.TextField()), 20 | ('child_safe', models.BooleanField(default=False)), 21 | ], 22 | options={ 23 | 'abstract': False, 24 | }, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /scss/pages/profile_detail.scss: -------------------------------------------------------------------------------- 1 | #query-info-div { 2 | margin-top: 15px; 3 | } 4 | 5 | #query-info-div .timestamp-div { 6 | font-size: 13px; 7 | 8 | } 9 | 10 | #pyprofile-div { 11 | display: block; 12 | margin: auto; 13 | width: 960px; 14 | } 15 | 16 | .pyprofile { 17 | text-align: left; 18 | } 19 | 20 | a { 21 | color: #45ADA8; 22 | } 23 | 24 | a:visited { 25 | color: #45ADA8; 26 | } 27 | 28 | a:hover { 29 | color: #547980; 30 | } 31 | 32 | a:active { 33 | color: #594F4F; 34 | } 35 | 36 | #graph-div { 37 | padding: 25px; 38 | background-color: white; 39 | display: block; 40 | margin-left: auto; 41 | margin-right: auto; 42 | margin-top: 25px; 43 | width: 960px; 44 | text-align: center; 45 | } 46 | 47 | #percent { 48 | width: 20px; 49 | } 50 | 51 | svg { 52 | display: block; 53 | } 54 | -------------------------------------------------------------------------------- /silk/static/silk/css/pages/cprofile.css: -------------------------------------------------------------------------------- 1 | #query-info-div { 2 | margin-top: 15px; 3 | } 4 | 5 | #query-info-div .timestamp-div { 6 | font-size: 13px; 7 | } 8 | 9 | #pyprofile-div { 10 | display: block; 11 | margin: auto; 12 | width: 960px; 13 | } 14 | 15 | .pyprofile { 16 | text-align: left; 17 | } 18 | 19 | a { 20 | color: #45ADA8; 21 | } 22 | 23 | a:visited { 24 | color: #45ADA8; 25 | } 26 | 27 | a:hover { 28 | color: #547980; 29 | } 30 | 31 | a:active { 32 | color: #594F4F; 33 | } 34 | 35 | #graph-div { 36 | padding: 25px; 37 | background-color: white; 38 | display: block; 39 | margin-left: auto; 40 | margin-right: auto; 41 | margin-top: 25px; 42 | width: 960px; 43 | text-align: center; 44 | } 45 | 46 | #percent { 47 | width: 20px; 48 | } 49 | 50 | svg { 51 | display: block; 52 | } 53 | -------------------------------------------------------------------------------- /project/tests/test_code_gen_django.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from unittest import TestCase 3 | 4 | from silk.code_generation.django_test_client import gen 5 | 6 | 7 | class TestCodeGenDjango(TestCase): 8 | def test_post(self): 9 | result = gen( 10 | path="/alpha/beta", 11 | method="POST", 12 | data={"gamma": "delta", "epsilon": "zeta"}, 13 | content_type="application/x-www-form-urlencoded", 14 | ) 15 | 16 | self.assertEqual(result, textwrap.dedent("""\ 17 | from django.test import Client 18 | c = Client() 19 | response = c.post(path='/alpha/beta', 20 | data={'gamma': 'delta', 'epsilon': 'zeta'}, 21 | content_type='application/x-www-form-urlencoded') 22 | """)) 23 | -------------------------------------------------------------------------------- /silk/static/silk/css/pages/profile_detail.css: -------------------------------------------------------------------------------- 1 | #query-info-div { 2 | margin-top: 15px; 3 | } 4 | 5 | #query-info-div .timestamp-div { 6 | font-size: 13px; 7 | } 8 | 9 | #pyprofile-div { 10 | display: block; 11 | margin: auto; 12 | width: 960px; 13 | } 14 | 15 | .pyprofile { 16 | text-align: left; 17 | } 18 | 19 | a { 20 | color: #45ADA8; 21 | } 22 | 23 | a:visited { 24 | color: #45ADA8; 25 | } 26 | 27 | a:hover { 28 | color: #547980; 29 | } 30 | 31 | a:active { 32 | color: #594F4F; 33 | } 34 | 35 | #graph-div { 36 | padding: 25px; 37 | background-color: white; 38 | display: block; 39 | margin-left: auto; 40 | margin-right: auto; 41 | margin-top: 25px; 42 | width: 960px; 43 | text-align: center; 44 | } 45 | 46 | #percent { 47 | width: 20px; 48 | } 49 | 50 | svg { 51 | display: block; 52 | } 53 | -------------------------------------------------------------------------------- /project/tests/test_compat.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest.mock import Mock 3 | 4 | from django.test import TestCase 5 | 6 | from silk.model_factory import ResponseModelFactory 7 | 8 | DJANGO_META_CONTENT_TYPE = 'CONTENT_TYPE' 9 | HTTP_CONTENT_TYPE = 'content-type' 10 | 11 | 12 | class TestByteStringCompatForResponse(TestCase): 13 | 14 | def test_bytes_compat(self): 15 | """ 16 | Test ResponseModelFactory formats json with bytes content 17 | """ 18 | mock = Mock() 19 | mock.headers = {HTTP_CONTENT_TYPE: 'application/json;'} 20 | d = {'k': 'v'} 21 | mock.content = bytes(json.dumps(d), 'utf-8') 22 | mock.get = mock.headers.get 23 | factory = ResponseModelFactory(mock) 24 | body, content = factory.body() 25 | self.assertDictEqual(json.loads(body), d) 26 | -------------------------------------------------------------------------------- /scss/pages/summary.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | margin-bottom: 20px; 4 | } 5 | 6 | .inner { 7 | margin: auto; 8 | width: 960px; 9 | } 10 | 11 | .summary-cell { 12 | display: inline-block; 13 | text-align: center; 14 | padding: 10px; 15 | margin-left: 10px; 16 | margin-top: 10px; 17 | } 18 | 19 | .summary-cell .desc { 20 | margin-top: 8px; 21 | font-size: 12px; 22 | } 23 | 24 | .no-data { 25 | font-size: 12px; 26 | font-style: oblique; 27 | margin-left: 10px; 28 | } 29 | 30 | h2 { 31 | margin-bottom: 0;; 32 | } 33 | 34 | #filters { 35 | margin-top: 10px; 36 | font-size: 12px; 37 | } 38 | 39 | #filters input, 40 | #filters span { 41 | color: black !important; 42 | font-weight: bold; 43 | } 44 | 45 | #filter-image { 46 | width: 20px; 47 | } 48 | 49 | #filter-cell { 50 | padding-left: 5px; 51 | } 52 | -------------------------------------------------------------------------------- /silk/static/silk/css/pages/summary.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | margin-bottom: 20px; 4 | } 5 | 6 | .inner { 7 | margin: auto; 8 | width: 960px; 9 | } 10 | 11 | .summary-cell { 12 | display: inline-block; 13 | text-align: center; 14 | padding: 10px; 15 | margin-left: 10px; 16 | margin-top: 10px; 17 | } 18 | 19 | .summary-cell .desc { 20 | margin-top: 8px; 21 | font-size: 12px; 22 | } 23 | 24 | .no-data { 25 | font-size: 12px; 26 | font-style: oblique; 27 | margin-left: 10px; 28 | } 29 | 30 | h2 { 31 | margin-bottom: 0; 32 | } 33 | 34 | #filters { 35 | margin-top: 10px; 36 | font-size: 12px; 37 | } 38 | 39 | #filters input, 40 | #filters span { 41 | color: black !important; 42 | font-weight: bold; 43 | } 44 | 45 | #filter-image { 46 | width: 20px; 47 | } 48 | 49 | #filter-cell { 50 | padding-left: 5px; 51 | } 52 | -------------------------------------------------------------------------------- /silk/static/silk/js/components/cell.js: -------------------------------------------------------------------------------- 1 | function configureSpanFontColors(selector, okValue, badValue) { 2 | selector.each(function () { 3 | var val = parseFloat($(this).text()); 4 | if (val < okValue) { 5 | $(this).addClass('very-good-font-color'); 6 | } 7 | else if (val < badValue) { 8 | $(this).addClass('ok-font-color'); 9 | } 10 | else { 11 | $(this).addClass('very-bad-font-color'); 12 | } 13 | }); 14 | } 15 | 16 | function configureFontColors() { 17 | configureSpanFontColors($('.time-taken-div .numeric'), 200, 500); 18 | configureSpanFontColors($('.time-taken-queries-div .numeric'), 50, 200); 19 | configureSpanFontColors($('.num-queries-div .numeric'), 10, 50); 20 | } 21 | 22 | $(document).ready(function () { 23 | configureFontColors(); 24 | }); 25 | -------------------------------------------------------------------------------- /project/example_app/templates/example_app/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% if form.errors %} 10 |

Your username and password didn't match. Please try again.

11 | {% endif %} 12 | 13 |
14 | {% csrf_token %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /scss/pages/sql.scss: -------------------------------------------------------------------------------- 1 | .right-aligned { 2 | text-align: right; 3 | 4 | } 5 | 6 | .center-aligned { 7 | text-align: center; 8 | 9 | } 10 | 11 | .left-aligned { 12 | text-align: left; 13 | } 14 | 15 | #table-pagination { 16 | text-align: center; 17 | margin: 20px; 18 | } 19 | 20 | table { 21 | border-spacing: 0; 22 | margin: auto; 23 | max-width: 920px; 24 | } 25 | 26 | tr { 27 | height: 20px; 28 | } 29 | 30 | tr.data-row:hover { 31 | background-color: rgb(51, 51, 68); 32 | color: white; 33 | cursor: pointer; 34 | } 35 | 36 | td { 37 | padding: 5px; 38 | padding-left: 20px; 39 | padding-right: 20px; 40 | } 41 | 42 | th { 43 | height: 40px; 44 | padding-left: 20px; 45 | padding-right: 20px; 46 | } 47 | 48 | #table-div { 49 | width: 100%; 50 | margin-top: 40px; 51 | } 52 | 53 | #table-pagination div { 54 | padding: 5px; 55 | } 56 | -------------------------------------------------------------------------------- /silk/static/silk/css/pages/sql.css: -------------------------------------------------------------------------------- 1 | .right-aligned { 2 | text-align: right; 3 | } 4 | 5 | .center-aligned { 6 | text-align: center; 7 | } 8 | 9 | .left-aligned { 10 | text-align: left; 11 | } 12 | 13 | #table-pagination { 14 | text-align: center; 15 | margin: 20px; 16 | } 17 | 18 | table { 19 | border-spacing: 0; 20 | margin: auto; 21 | max-width: 920px; 22 | } 23 | 24 | tr { 25 | height: 20px; 26 | } 27 | 28 | tr.data-row:hover { 29 | background-color: rgb(51, 51, 68); 30 | color: white; 31 | cursor: pointer; 32 | } 33 | 34 | td { 35 | padding: 5px; 36 | padding-left: 20px; 37 | padding-right: 20px; 38 | } 39 | 40 | th { 41 | height: 40px; 42 | padding-left: 20px; 43 | padding-right: 20px; 44 | } 45 | 46 | #table-div { 47 | width: 100%; 48 | margin-top: 40px; 49 | } 50 | 51 | #table-pagination div { 52 | padding: 5px; 53 | } 54 | -------------------------------------------------------------------------------- /project/tests/test_multipart_forms.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | from django.test import TestCase 4 | from django.urls import reverse 5 | 6 | from silk.model_factory import RequestModelFactory, multipart_form 7 | 8 | 9 | class TestMultipartForms(TestCase): 10 | 11 | def test_no_max_request(self): 12 | mock_request = Mock() 13 | mock_request.headers = {'content-type': multipart_form} 14 | mock_request.GET = {} 15 | mock_request.path = reverse('silk:requests') 16 | mock_request.method = 'post' 17 | mock_request.body = Mock() 18 | request_model = RequestModelFactory(mock_request).construct_request_model() 19 | self.assertFalse(request_model.body) 20 | self.assertEqual(b"Raw body not available for multipart_form data, Silk is not showing file uploads.", request_model.raw_body) 21 | mock_request.body.assert_not_called() 22 | -------------------------------------------------------------------------------- /project/project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.contrib.auth import views 5 | from django.urls import include, path 6 | 7 | urlpatterns = [ 8 | path( 9 | route='silk/', 10 | view=include('silk.urls', namespace='silk'), 11 | ), 12 | path( 13 | route='example_app/', 14 | view=include('example_app.urls', namespace='example_app'), 15 | ), 16 | path(route='admin/', view=admin.site.urls), 17 | ] 18 | 19 | 20 | urlpatterns += [ 21 | path( 22 | route='login/', 23 | view=views.LoginView.as_view( 24 | template_name='example_app/login.html' 25 | ), 26 | name='login', 27 | ), 28 | ] 29 | 30 | 31 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + \ 32 | static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 33 | -------------------------------------------------------------------------------- /silk/templates/silk/base/detail_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'silk/base/base.html' %} 2 | {% load static %} 3 | {% block style %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block js %} 13 | 14 | 15 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /project/tests/test_command_garbage_collect.py: -------------------------------------------------------------------------------- 1 | from django.core import management 2 | from django.test import TestCase 3 | 4 | from silk import models 5 | from silk.config import SilkyConfig 6 | 7 | from .factories import RequestMinFactory 8 | 9 | 10 | class TestViewClearDB(TestCase): 11 | def test_garbage_collect_command(self): 12 | SilkyConfig().SILKY_MAX_RECORDED_REQUESTS = 2 13 | RequestMinFactory.create_batch(3) 14 | self.assertEqual(models.Request.objects.count(), 3) 15 | management.call_command("silk_request_garbage_collect") 16 | self.assertEqual(models.Request.objects.count(), 2) 17 | management.call_command("silk_request_garbage_collect", max_requests=1) 18 | self.assertEqual(models.Request.objects.count(), 1) 19 | management.call_command( 20 | "silk_request_garbage_collect", max_requests=0, verbosity=2 21 | ) 22 | self.assertEqual(models.Request.objects.count(), 0) 23 | -------------------------------------------------------------------------------- /silk/templates/silk/inclusion/profile_summary.html: -------------------------------------------------------------------------------- 1 | {% load silk_filters %} 2 |
3 |
{{ profile.start_time | silk_date_time }}
4 |
{% if profile.name %}{{ profile.name }}{% elif profile.func_name %}{{ profile.func_name }}{% endif %}
5 |
{{ profile.path }}
6 |
7 | {{ profile.time_taken|floatformat:"0" }}ms 8 | overall 9 |
10 |
11 | {{ profile.time_spent_on_sql_queries|floatformat:"0" }}ms 12 | on queries
13 |
14 | {{ profile.queries.count }} 15 | queries 16 |
17 |
18 | -------------------------------------------------------------------------------- /scss/pages/sql_detail.scss: -------------------------------------------------------------------------------- 1 | #traceback { 2 | width: 960px; 3 | margin: auto; 4 | } 5 | 6 | #traceback pre { 7 | margin-top: 15px !important; 8 | margin-bottom: 15px !important; 9 | } 10 | 11 | #traceback .not-third-party { 12 | font-weight: bold; 13 | } 14 | 15 | a:hover { 16 | color: #9dd0ff; 17 | } 18 | 19 | a:active { 20 | color: #594F4F; 21 | } 22 | 23 | code { 24 | background-color: transparent !important; 25 | } 26 | 27 | #query-div pre { 28 | background-color: transparent !important; 29 | } 30 | 31 | #query-div { 32 | padding-top: 15px; 33 | } 34 | 35 | #query-info-div div { 36 | padding-top: 5px; 37 | } 38 | 39 | #query-plan-div { 40 | text-align: left; 41 | width: 960px; 42 | margin: auto; 43 | } 44 | 45 | #plan-div code { 46 | margin: auto !important; 47 | display: inline-block; 48 | } 49 | 50 | #query-plan-head { 51 | padding-top: 5px; 52 | padding-bottom: 15px; 53 | text-align: center; 54 | margin: auto; 55 | } 56 | 57 | .file-path { 58 | font-size: 13px; 59 | } 60 | -------------------------------------------------------------------------------- /project/example_app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import reverse 3 | 4 | from .models import Blind 5 | 6 | 7 | @admin.register(Blind) 8 | class BlindAdmin(admin.ModelAdmin): 9 | list_display = ('desc', 'thumbnail', 'name', 'child_safe') 10 | list_editable = ('name', 'child_safe') 11 | 12 | @admin.display( 13 | description='Photo' 14 | ) 15 | def thumbnail(self, obj): 16 | try: 17 | img_tag = '' % obj.photo.url 18 | except ValueError: 19 | return '' 20 | url = self._blind_url(obj) 21 | return f'{img_tag}' 22 | 23 | def _blind_url(self, obj): 24 | url = reverse('admin:example_app_blind_change', args=(obj.id, )) 25 | return url 26 | 27 | @admin.display( 28 | description='Blind' 29 | ) 30 | def desc(self, obj): 31 | desc = str(obj) 32 | url = self._blind_url(obj) 33 | return f'{desc}' 34 | -------------------------------------------------------------------------------- /silk/static/silk/css/pages/sql_detail.css: -------------------------------------------------------------------------------- 1 | #traceback { 2 | width: 960px; 3 | margin: auto; 4 | } 5 | 6 | #traceback pre { 7 | margin-top: 15px !important; 8 | margin-bottom: 15px !important; 9 | } 10 | 11 | #traceback .not-third-party { 12 | font-weight: bold; 13 | } 14 | 15 | a:hover { 16 | color: #9dd0ff; 17 | } 18 | 19 | a:active { 20 | color: #594F4F; 21 | } 22 | 23 | code { 24 | background-color: transparent !important; 25 | } 26 | 27 | #query-div pre { 28 | background-color: transparent !important; 29 | } 30 | 31 | #query-div { 32 | padding-top: 15px; 33 | } 34 | 35 | #query-info-div div { 36 | padding-top: 5px; 37 | } 38 | 39 | #query-plan-div { 40 | text-align: left; 41 | width: 960px; 42 | margin: auto; 43 | } 44 | 45 | #plan-div code { 46 | margin: auto !important; 47 | display: inline-block; 48 | } 49 | 50 | #query-plan-head { 51 | padding-top: 5px; 52 | padding-bottom: 15px; 53 | text-align: center; 54 | margin: auto; 55 | } 56 | 57 | .file-path { 58 | font-size: 13px; 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Michael Ford 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /silk/templates/silk/inclusion/root_menu.html: -------------------------------------------------------------------------------- 1 | {% load silk_nav %} 2 | 9 | 16 | 23 | 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | if: github.repository == 'jazzband/django-silk' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: 3.14 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install -U pip 26 | python -m pip install -U setuptools twine wheel 27 | 28 | - name: Build package 29 | run: | 30 | python setup.py --version 31 | python setup.py sdist --format=gztar bdist_wheel 32 | twine check dist/* 33 | 34 | - name: Upload packages to Jazzband 35 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 36 | uses: pypa/gh-action-pypi-publish@master 37 | with: 38 | user: jazzband 39 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 40 | repository_url: https://jazzband.co/projects/django-silk/upload 41 | -------------------------------------------------------------------------------- /silk/management/commands/silk_request_garbage_collect.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | import silk.models 4 | from silk.config import SilkyConfig 5 | 6 | 7 | class Command(BaseCommand): 8 | help = "Triggers silk's request garbage collect." 9 | 10 | def add_arguments(self, parser): 11 | parser.add_argument( 12 | "-m", 13 | "--max-requests", 14 | default=SilkyConfig().SILKY_MAX_RECORDED_REQUESTS, 15 | type=int, 16 | help="Maximum number of requests to keep after garbage collection.", 17 | ) 18 | 19 | def handle(self, *args, **options): 20 | if "max_requests" in options: 21 | max_requests = options["max_requests"] 22 | SilkyConfig().SILKY_MAX_RECORDED_REQUESTS = max_requests 23 | if options["verbosity"] >= 2: 24 | max_requests = SilkyConfig().SILKY_MAX_RECORDED_REQUESTS 25 | request_count = silk.models.Request.objects.count() 26 | self.stdout.write( 27 | f"Keeping up to {max_requests} of {request_count} requests." 28 | ) 29 | silk.models.Request.garbage_collect(force=True) 30 | -------------------------------------------------------------------------------- /project/tests/test_config_long_urls.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | from django.test import TestCase 4 | 5 | from silk.model_factory import RequestModelFactory 6 | 7 | 8 | class TestLongRequestUrl(TestCase): 9 | 10 | def test_no_long_url(self): 11 | url = '1234567890' * 19 # 190-character URL 12 | mock_request = Mock() 13 | mock_request.headers = {'content-type': 'text/plain'} 14 | mock_request.GET = {} 15 | mock_request.path = url 16 | mock_request.method = 'get' 17 | request_model = RequestModelFactory(mock_request).construct_request_model() 18 | self.assertEqual(request_model.path, url) 19 | 20 | def test_long_url(self): 21 | url = '1234567890' * 200 # 2000-character URL 22 | mock_request = Mock() 23 | mock_request.headers = {'content-type': 'text/plain'} 24 | mock_request.GET = {} 25 | mock_request.method = 'get' 26 | mock_request.path = url 27 | request_model = RequestModelFactory(mock_request).construct_request_model() 28 | self.assertEqual(request_model.path, f'{url[:94]}...{url[1907:]}') 29 | self.assertEqual(len(request_model.path), 190) 30 | -------------------------------------------------------------------------------- /silk/auth.py: -------------------------------------------------------------------------------- 1 | from functools import WRAPPER_ASSIGNMENTS, wraps 2 | 3 | from django.contrib.auth.decorators import login_required 4 | from django.core.exceptions import PermissionDenied 5 | 6 | from silk.config import SilkyConfig 7 | 8 | 9 | def login_possibly_required(function=None, **kwargs): 10 | if SilkyConfig().SILKY_AUTHENTICATION: 11 | return login_required(function, **kwargs) 12 | return function 13 | 14 | 15 | def permissions_possibly_required(function=None): 16 | if SilkyConfig().SILKY_AUTHORISATION: 17 | actual_decorator = user_passes_test( 18 | SilkyConfig().SILKY_PERMISSIONS 19 | ) 20 | if function: 21 | return actual_decorator(function) 22 | return actual_decorator 23 | return function 24 | 25 | 26 | def user_passes_test(test_func): 27 | def decorator(view_func): 28 | @wraps(view_func, assigned=WRAPPER_ASSIGNMENTS) 29 | def _wrapped_view(request, *args, **kwargs): 30 | if test_func(request.user): 31 | return view_func(request, *args, **kwargs) 32 | else: 33 | raise PermissionDenied 34 | 35 | return _wrapped_view 36 | 37 | return decorator 38 | -------------------------------------------------------------------------------- /silk/static/silk/css/pages/request.css: -------------------------------------------------------------------------------- 1 | pre { 2 | white-space: pre-wrap; /* css-3 */ 3 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 4 | /*noinspection CssInvalidElement*/ 5 | white-space: -pre-wrap; /* Opera 4-6 */ 6 | white-space: -o-pre-wrap; /* Opera 7 */ 7 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 8 | } 9 | 10 | .cell { 11 | background-color: transparent; 12 | margin-top: 15px; 13 | } 14 | 15 | div.wrapper { 16 | width: 100%; 17 | } 18 | 19 | div.wrapper div#request-summary { 20 | margin: auto; 21 | text-align: center; 22 | width: 100%; 23 | } 24 | 25 | div.wrapper div#request-info { 26 | width: 960px; 27 | margin: auto auto 20px; 28 | } 29 | 30 | a { 31 | color: #45ADA8; 32 | } 33 | 34 | a:visited { 35 | color: #45ADA8; 36 | } 37 | 38 | a:hover { 39 | color: #547980; 40 | } 41 | 42 | a:active { 43 | color: #594F4F; 44 | } 45 | 46 | .headers { 47 | font-size: 12px; 48 | font-family: Fantasque; 49 | background-color: white; 50 | width: 100%; 51 | } 52 | 53 | .headers tr:hover { 54 | background-color: #f4f4f4; 55 | } 56 | 57 | .headers td { 58 | padding-bottom: 5px; 59 | padding-left: 5px; 60 | } 61 | 62 | .headers .key { 63 | font-weight: bold; 64 | } 65 | -------------------------------------------------------------------------------- /scss/pages/request.scss: -------------------------------------------------------------------------------- 1 | pre { 2 | white-space: pre-wrap; /* css-3 */ 3 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 4 | /*noinspection CssInvalidElement*/ 5 | white-space: -pre-wrap; /* Opera 4-6 */ 6 | white-space: -o-pre-wrap; /* Opera 7 */ 7 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 8 | } 9 | 10 | #request-summary { 11 | 12 | } 13 | 14 | .cell { 15 | background-color: transparent; 16 | margin-top: 15px; 17 | } 18 | 19 | div.wrapper { 20 | width: 100%; 21 | } 22 | 23 | div.wrapper div#request-summary { 24 | margin: auto; 25 | text-align: center; 26 | width: 100%; 27 | } 28 | 29 | div.wrapper div#request-info { 30 | width: 960px; 31 | margin: auto auto 20px; 32 | } 33 | 34 | a { 35 | color: #45ADA8; 36 | } 37 | 38 | a:visited { 39 | color: #45ADA8; 40 | } 41 | 42 | a:hover { 43 | color: #547980; 44 | } 45 | 46 | a:active { 47 | color: #594F4F; 48 | } 49 | 50 | .headers { 51 | font-size: 12px; 52 | font-family: Fantasque; 53 | background-color: white; 54 | width: 100%; 55 | } 56 | 57 | .headers tr:hover { 58 | background-color: #f4f4f4; 59 | } 60 | 61 | .headers td { 62 | padding-bottom: 5px; 63 | padding-left: 5px; 64 | } 65 | 66 | .headers .key { 67 | font-weight: bold; 68 | } 69 | 70 | .headers .value { 71 | 72 | } 73 | -------------------------------------------------------------------------------- /silk/utils/data_deletion.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import connections 3 | 4 | 5 | def delete_model(model): 6 | engine = settings.DATABASES[model.objects.db]['ENGINE'] 7 | table = model._meta.db_table 8 | if 'mysql' in engine or 'postgresql' in engine: 9 | # Use "TRUNCATE" on the table 10 | with connections[model.objects.db].cursor() as cursor: 11 | if 'mysql' in engine: 12 | cursor.execute("SET FOREIGN_KEY_CHECKS=0;") 13 | cursor.execute(f"TRUNCATE TABLE {table}") 14 | cursor.execute("SET FOREIGN_KEY_CHECKS=1;") 15 | elif 'postgres' in engine: 16 | cursor.execute(f"ALTER TABLE {table} DISABLE TRIGGER USER;") 17 | cursor.execute(f"TRUNCATE TABLE {table} CASCADE") 18 | cursor.execute(f"ALTER TABLE {table} ENABLE TRIGGER USER;") 19 | return 20 | 21 | # Manually delete rows because sqlite does not support TRUNCATE and 22 | # oracle doesn't provide good support for disabling foreign key checks 23 | while True: 24 | items_to_delete = list( 25 | model.objects.values_list('pk', flat=True).all()[:800]) 26 | if not items_to_delete: 27 | break 28 | model.objects.filter(pk__in=items_to_delete).delete() 29 | -------------------------------------------------------------------------------- /project/tests/util.py: -------------------------------------------------------------------------------- 1 | import io 2 | from unittest.mock import Mock 3 | 4 | from django.core.files import File 5 | from django.core.files.storage import Storage 6 | 7 | from silk.models import Request 8 | 9 | 10 | def mock_data_collector(): 11 | mock = Mock() 12 | mock.queries = [] 13 | mock.local = Mock() 14 | r = Request() 15 | mock.local.request = r 16 | mock.request = r 17 | return mock 18 | 19 | 20 | def delete_all_models(model_class): 21 | """ 22 | A sqlite3-safe deletion function to avoid "django.db.utils.OperationalError: too many SQL variables" 23 | :param model_class: 24 | :return: 25 | """ 26 | while model_class.objects.count(): 27 | ids = model_class.objects.values_list('pk', flat=True)[:80] 28 | model_class.objects.filter(pk__in=ids).delete() 29 | 30 | 31 | class DictStorage(Storage): 32 | """Storage that stores files in a dictionary - for testing.""" 33 | 34 | def __init__(self): 35 | self.files = {} 36 | 37 | def open(self, name, mode="rb"): 38 | if name not in self.files: 39 | self.files[name] = b"" 40 | return File(io.BytesIO(self.files[name]), name=name) 41 | 42 | def get_valid_name(self, name): 43 | return name 44 | 45 | def exists(self, name): 46 | return name in self.files 47 | -------------------------------------------------------------------------------- /project/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | import factory.fuzzy 3 | from example_app.models import Blind 4 | 5 | from silk.models import Request, Response, SQLQuery 6 | 7 | HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'HEAD', 'OPTIONS'] 8 | STATUS_CODES = [200, 201, 300, 301, 302, 401, 403, 404] 9 | 10 | 11 | class SQLQueryFactory(factory.django.DjangoModelFactory): 12 | 13 | query = factory.Sequence(lambda num: 'SELECT foo FROM bar WHERE foo=%s' % num) 14 | traceback = factory.Sequence(lambda num: 'Traceback #%s' % num) 15 | 16 | class Meta: 17 | model = SQLQuery 18 | 19 | 20 | class RequestMinFactory(factory.django.DjangoModelFactory): 21 | 22 | path = factory.Faker('uri_path') 23 | method = factory.fuzzy.FuzzyChoice(HTTP_METHODS) 24 | 25 | class Meta: 26 | model = Request 27 | 28 | 29 | class ResponseFactory(factory.django.DjangoModelFactory): 30 | request = factory.SubFactory(RequestMinFactory) 31 | status_code = factory.fuzzy.FuzzyChoice(STATUS_CODES) 32 | 33 | class Meta: 34 | model = Response 35 | 36 | 37 | class BlindFactory(factory.django.DjangoModelFactory): 38 | name = factory.Faker('pystr', min_chars=5, max_chars=10) 39 | child_safe = factory.Faker('pybool') 40 | photo = factory.django.ImageField() 41 | 42 | class Meta: 43 | model = Blind 44 | -------------------------------------------------------------------------------- /silk/views/raw.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.http import HttpResponse 4 | from django.shortcuts import render 5 | from django.utils.decorators import method_decorator 6 | from django.views.generic import View 7 | 8 | from silk.auth import login_possibly_required, permissions_possibly_required 9 | from silk.models import Request 10 | 11 | Logger = logging.getLogger('silk.views.raw') 12 | 13 | 14 | class Raw(View): 15 | 16 | @method_decorator(login_possibly_required) 17 | @method_decorator(permissions_possibly_required) 18 | def get(self, request, request_id): 19 | typ = request.GET.get('typ', None) 20 | subtyp = request.GET.get('subtyp', None) 21 | body = None 22 | if typ and subtyp: 23 | silk_request = Request.objects.get(pk=request_id) 24 | if typ == 'request': 25 | body = silk_request.raw_body if subtyp == 'raw' else silk_request.body 26 | elif typ == 'response': 27 | Logger.debug(silk_request.response.raw_body_decoded) 28 | body = silk_request.response.raw_body_decoded if subtyp == 'raw' else silk_request.response.body 29 | return render(request, 'silk/raw.html', { 30 | 'body': body 31 | }) 32 | else: 33 | return HttpResponse(content='Bad Request', status=400) 34 | -------------------------------------------------------------------------------- /silk/static/silk/css/components/row.css: -------------------------------------------------------------------------------- 1 | .row-wrapper { 2 | display: table; 3 | margin: 2rem; 4 | width: 100%; 5 | width: -moz-available; 6 | width: -webkit-fill-available; 7 | width: fill-available; 8 | } 9 | .row-wrapper .row { 10 | display: table-row; 11 | transition: background-color 0.15s ease, color 0.15s ease; 12 | } 13 | .row-wrapper .row div { 14 | padding: 1rem; 15 | } 16 | .row-wrapper .row .col { 17 | font-size: 20px; 18 | display: table-cell; 19 | } 20 | .row-wrapper .row .timestamp-div { 21 | border-top-left-radius: 4px; 22 | border-bottom-left-radius: 4px; 23 | margin-bottom: 15px; 24 | font-size: 13px; 25 | } 26 | .row-wrapper .row .meta { 27 | font-size: 12px; 28 | color: #be5b43; 29 | } 30 | .row-wrapper .row .meta .unit { 31 | font-size: 9px; 32 | font-weight: lighter !important; 33 | } 34 | .row-wrapper .row .method-div { 35 | font-weight: bold; 36 | font-size: 20px; 37 | } 38 | .row-wrapper .row .path-div { 39 | font-size: 18px; 40 | margin-bottom: 15px; 41 | } 42 | .row-wrapper .row .num-queries-div { 43 | border-top-right-radius: 4px; 44 | border-bottom-right-radius: 4px; 45 | } 46 | .row-wrapper .row .spacing .numeric { 47 | padding: 0 0.3rem; 48 | } 49 | .row-wrapper .row .spacing .meta { 50 | padding: 0 0.3rem; 51 | } 52 | .row-wrapper .row:hover { 53 | background-color: rgb(51, 51, 68); 54 | color: white; 55 | cursor: pointer; 56 | } 57 | -------------------------------------------------------------------------------- /silk/templates/silk/inclusion/request_summary.html: -------------------------------------------------------------------------------- 1 | {% load silk_filters %} 2 |
3 |
{{ silk_request.start_time | silk_date_time }}
4 |
{% if silk_request.response.status_code %}{{ silk_request.response.status_code }} {% endif %}{{ silk_request.method }}
5 |
{{ silk_request.path }}
6 |
7 | {{ silk_request.time_taken|floatformat:"0" }}ms 8 | overall{% if silk_request.total_meta_time %} +{{ silk_request.total_meta_time | floatformat:"0" }}ms{% endif %} 9 |
10 |
11 | {{ silk_request.time_spent_on_sql_queries|floatformat:"0" }}ms 12 | on queries{% if silk_request.meta_time_spent_queries %} +{{ silk_request.meta_time_spent_queries | floatformat:"0" }}ms{% endif %}
13 |
14 | {{ silk_request.num_sql_queries }} 15 | queries{% if silk_request.meta_num_queries %} +{{ silk_request.meta_num_queries }}{% endif %} 16 |
17 |
18 | -------------------------------------------------------------------------------- /scss/components/row.scss: -------------------------------------------------------------------------------- 1 | .row-wrapper { 2 | display: table; 3 | margin: 2rem; 4 | width: 100%; 5 | width: -moz-available; 6 | width: -webkit-fill-available; 7 | width: fill-available; 8 | 9 | .row { 10 | display: table-row; 11 | transition: background-color 0.15s ease, color 0.15s ease; 12 | 13 | div { 14 | padding: 1rem; 15 | } 16 | 17 | .col { 18 | font-size: 20px; 19 | display: table-cell; 20 | } 21 | 22 | .timestamp-div { 23 | border-top-left-radius: 4px; 24 | border-bottom-left-radius: 4px; 25 | margin-bottom: 15px; 26 | font-size: 13px; 27 | } 28 | 29 | .meta { 30 | font-size: 12px; 31 | color: #be5b43; 32 | } 33 | 34 | .meta .unit { 35 | font-size: 9px; 36 | font-weight: lighter !important; 37 | } 38 | 39 | .method-div { 40 | font-weight: bold; 41 | font-size: 20px; 42 | } 43 | 44 | .path-div { 45 | font-size: 18px; 46 | margin-bottom: 15px; 47 | } 48 | 49 | .num-queries-div { 50 | border-top-right-radius: 4px; 51 | border-bottom-right-radius: 4px; 52 | } 53 | 54 | .spacing { 55 | .numeric { 56 | padding: 0 0.3rem; 57 | } 58 | .meta { 59 | padding: 0 0.3rem; 60 | } 61 | } 62 | 63 | } 64 | 65 | .row:hover { 66 | background-color: rgb(51, 51, 68); 67 | color: white; 68 | cursor: pointer; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. silk documentation master file, created by 2 | sphinx-quickstart on Sun Jun 22 13:51:12 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Silk 7 | ================================ 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | quickstart 13 | profiling 14 | configuration 15 | troubleshooting 16 | 17 | Silk is a live profiling and inspection tool for the Django framework. Silk intercepts and stores HTTP requests and database queries before presenting them in a user interface for further inspection: 18 | 19 | .. image:: /images/1.png 20 | 21 | A **live demo** is available `here`_. 22 | 23 | .. _here: http://mtford.co.uk/silk/ 24 | 25 | Features 26 | -------- 27 | 28 | - Inspect HTTP requests and responses 29 | 30 | - Query parameters 31 | 32 | - Headers 33 | 34 | - Bodies 35 | 36 | - Execution Time 37 | 38 | - Database Queries 39 | 40 | - Number 41 | 42 | - Time taken 43 | 44 | - SQL query inspection 45 | 46 | - Profiling of arbitrary code blocks via a Python context manager and decorator 47 | 48 | - Execution Time 49 | 50 | - Database Queries 51 | 52 | - Can also be injected dynamically at runtime e.g. if read-only dependency. 53 | 54 | - Authentication/Authorisation for production use 55 | 56 | 57 | Requirements 58 | ------------ 59 | 60 | * Django: 4.2, 5.1, 5.2, 6.0 61 | * Python: 3.10, 3.11, 3.12, 3.13, 3.14 62 | -------------------------------------------------------------------------------- /silk/templates/silk/inclusion/request_summary_row.html: -------------------------------------------------------------------------------- 1 | {% load silk_filters %} 2 |
{{ silk_request.start_time | silk_date_time }}
3 |
{% if silk_request.response.status_code %}{{ silk_request.response.status_code }} {% endif %}{{ silk_request.method }}
4 |
{{ silk_request.path }}
5 |
6 | {{ silk_request.time_taken|floatformat:"0" }}ms 7 | overall{% if silk_request.total_meta_time %} +{{ silk_request.total_meta_time | floatformat:"0" }}ms{% endif %} 8 |
9 |
10 | {{ silk_request.time_spent_on_sql_queries|floatformat:"0" }}ms 11 | on queries{% if silk_request.meta_time_spent_queries %} +{{ silk_request.meta_time_spent_queries | floatformat:"0" }}ms{% endif %}
12 |
13 | {{ silk_request.num_sql_queries }} 14 | queries{% if silk_request.meta_num_queries %} +{{ silk_request.meta_num_queries }}{% endif %} 15 |
16 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [gh-actions] 2 | python = 3 | 3.10: py310 4 | 3.11: py311 5 | 3.12: py312 6 | 3.13: py313 7 | 3.14: py314 8 | 9 | [gh-actions:env] 10 | DJANGO = 11 | 4.2: dj42 12 | 5.1: dj51 13 | 5.2: dj52 14 | 6.0: dj60 15 | main: djmain 16 | 17 | [tox] 18 | envlist = 19 | py{310,311,312,313,314}-dj{42,50,51,52}-{sqlite3,mysql,postgresql} 20 | py{312,313,314}-dj{60,main}-{sqlite3,mysql,postgresql} 21 | 22 | [testenv] 23 | usedevelop = True 24 | ignore_outcome = 25 | djmain: True 26 | changedir = {toxinidir}/project 27 | deps = 28 | -rrequirements.txt 29 | mysql: mysqlclient 30 | postgresql: psycopg2-binary 31 | dj42: django>=4.2,<4.3 32 | dj51: django>=5.1,<5.2 33 | dj52: django>=5.2,<5.3 34 | dj60: django>=6.0,<6.1 35 | djmain: https://github.com/django/django/archive/main.tar.gz 36 | py312: setuptools 37 | py313: setuptools 38 | py314: setuptools 39 | extras = formatting 40 | setenv = 41 | PYTHONPATH={toxinidir}:{toxinidir} 42 | PYTHONDONTWRITEBYTECODE=1 43 | sqlite3: DB_ENGINE=sqlite3 44 | sqlite3: DB_NAME=":memory:" 45 | mysql: DB_ENGINE=mysql 46 | mysql: DB_NAME=mysql 47 | mysql: DB_USER=root 48 | mysql: DB_PASSWORD=mysql 49 | mysql: DB_PORT=3306 50 | postgresql: DB_ENGINE=postgresql 51 | postgresql: DB_NAME=postgres 52 | postgresql: DB_PASSWORD=postgres 53 | commands = pytest 54 | 55 | [flake8] 56 | ignore = 57 | E501, 58 | E203, 59 | W503 60 | -------------------------------------------------------------------------------- /project/tests/test_view_clear_db.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from silk import models 4 | from silk.config import SilkyConfig 5 | from silk.middleware import silky_reverse 6 | 7 | from .factories import RequestMinFactory 8 | 9 | 10 | class TestViewClearDB(TestCase): 11 | @classmethod 12 | def setUpClass(cls): 13 | super().setUpClass() 14 | SilkyConfig().SILKY_AUTHENTICATION = False 15 | SilkyConfig().SILKY_AUTHORISATION = False 16 | 17 | def test_clear_all(self): 18 | RequestMinFactory.create() 19 | self.assertEqual(models.Request.objects.count(), 1) 20 | response = self.client.post(silky_reverse("cleardb"), {"clear_all": "on"}) 21 | self.assertTrue(response.status_code == 200) 22 | self.assertEqual(models.Request.objects.count(), 0) 23 | 24 | 25 | class TestViewClearDBAndDeleteProfiles(TestCase): 26 | @classmethod 27 | def setUpClass(cls): 28 | super().setUpClass() 29 | SilkyConfig().SILKY_AUTHENTICATION = False 30 | SilkyConfig().SILKY_AUTHORISATION = False 31 | SilkyConfig().SILKY_DELETE_PROFILES = True 32 | 33 | def test_clear_all_and_delete_profiles(self): 34 | RequestMinFactory.create() 35 | self.assertEqual(models.Request.objects.count(), 1) 36 | response = self.client.post(silky_reverse("cleardb"), {"clear_all": "on"}) 37 | self.assertTrue(response.status_code == 200) 38 | self.assertEqual(models.Request.objects.count(), 0) 39 | -------------------------------------------------------------------------------- /silk/static/silk/js/components/filters.js: -------------------------------------------------------------------------------- 1 | 2 | function configureResizingInputs() { 3 | var $inputs = $('.resizing-input'); 4 | 5 | function resizeForText(text) { 6 | var $this = $(this); 7 | if (!text.trim()) { 8 | text = $this.attr('placeholder').trim(); 9 | } 10 | var $span = $this.parent().find('span'); 11 | $span.text(text); 12 | var $inputSize = $span.width(); 13 | $this.css("width", $inputSize); 14 | } 15 | 16 | $inputs.find('input').keypress(function (e) { 17 | if (e.which && e.charCode) { 18 | var c = String.fromCharCode(e.keyCode | e.charCode); 19 | var $this = $(this); 20 | resizeForText.call($this, $this.val() + c); 21 | } 22 | }); 23 | 24 | $inputs.find('input').keyup(function (e) { // Backspace event only fires for keyup 25 | if (e.keyCode === 8 || e.keyCode === 46) { 26 | resizeForText.call($(this), $(this).val()); 27 | } 28 | }); 29 | 30 | $inputs.find('input').each(function () { 31 | var $this = $(this); 32 | resizeForText.call($this, $this.val()) 33 | }); 34 | 35 | 36 | $('.resizing-input .datetimepicker').datetimepicker({ 37 | step: 10, 38 | onChangeDateTime: function (dp, $input) { 39 | resizeForText.call($input, $input.val()) 40 | } 41 | }); 42 | 43 | } 44 | 45 | /** 46 | * Entry point for filter initialisation. 47 | */ 48 | function initFilters() { 49 | configureResizingInputs(); 50 | } 51 | -------------------------------------------------------------------------------- /silk/templates/silk/inclusion/request_menu.html: -------------------------------------------------------------------------------- 1 | {% load silk_nav %} 2 | 6 | 11 | 12 | 13 | 18 | 19 | 20 | 25 | 26 | 27 | 32 | 33 | 34 | 35 | 40 | 41 | -------------------------------------------------------------------------------- /project/tests/test_view_summary_view.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from silk.middleware import silky_reverse 4 | from silk.views.summary import SummaryView 5 | 6 | from .test_lib.assertion import dict_contains 7 | from .test_lib.mock_suite import MockSuite 8 | 9 | mock_suite = MockSuite() 10 | 11 | 12 | class TestSummaryView(TestCase): 13 | def test_longest_query_by_view(self): 14 | [mock_suite.mock_request() for _ in range(0, 10)] 15 | print([x.time_taken for x in SummaryView()._longest_query_by_view([])]) 16 | 17 | def test_view_without_session_and_auth_middlewares(self): 18 | """ 19 | Filters are not present because there is no `session` to store them. 20 | """ 21 | with self.modify_settings(MIDDLEWARE={ 22 | 'remove': [ 23 | 'django.contrib.sessions.middleware.SessionMiddleware', 24 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 25 | 'django.contrib.messages.middleware.MessageMiddleware', 26 | ], 27 | }): 28 | # test filters on POST 29 | seconds = 3600 30 | response = self.client.post(silky_reverse('summary'), { 31 | 'filter-seconds-value': seconds, 32 | 'filter-seconds-typ': 'SecondsFilter', 33 | }) 34 | context = response.context 35 | self.assertTrue(dict_contains({ 36 | 'filters': { 37 | 'seconds': {'typ': 'SecondsFilter', 'value': seconds, 'str': f'>{seconds} seconds ago'} 38 | } 39 | }, context)) 40 | -------------------------------------------------------------------------------- /silk/templatetags/silk_inclusion.py: -------------------------------------------------------------------------------- 1 | from django.template import Library 2 | 3 | register = Library() 4 | 5 | 6 | def request_summary(silk_request): 7 | return {'silk_request': silk_request} 8 | 9 | 10 | def request_summary_row(silk_request): 11 | return {'silk_request': silk_request} 12 | 13 | 14 | def request_menu(request, silk_request): 15 | return {'request': request, 16 | 'silk_request': silk_request} 17 | 18 | 19 | def root_menu(request): 20 | return {'request': request} 21 | 22 | 23 | def profile_menu(request, profile, silk_request=None): 24 | context = {'request': request, 'profile': profile} 25 | if silk_request: 26 | context['silk_request'] = silk_request 27 | return context 28 | 29 | 30 | def profile_summary(profile): 31 | return {'profile': profile} 32 | 33 | 34 | def heading(text): 35 | return {'text': text} 36 | 37 | 38 | def code(lines, actual_line): 39 | return {'code': lines, 'actual_line': [x.strip() for x in actual_line]} 40 | 41 | 42 | register.inclusion_tag('silk/inclusion/request_summary.html')(request_summary) 43 | register.inclusion_tag('silk/inclusion/request_summary_row.html')(request_summary_row) 44 | register.inclusion_tag('silk/inclusion/profile_summary.html')(profile_summary) 45 | register.inclusion_tag('silk/inclusion/code.html')(code) 46 | register.inclusion_tag('silk/inclusion/request_menu.html')(request_menu) 47 | register.inclusion_tag('silk/inclusion/profile_menu.html')(profile_menu) 48 | register.inclusion_tag('silk/inclusion/root_menu.html')(root_menu) 49 | register.inclusion_tag('silk/inclusion/heading.html')(heading) 50 | -------------------------------------------------------------------------------- /silk/views/code.py: -------------------------------------------------------------------------------- 1 | from silk.config import SilkyConfig 2 | 3 | __author__ = 'mtford' 4 | 5 | 6 | def _code(file_path, line_num, end_line_num=None): 7 | line_num = int(line_num) 8 | if not end_line_num: 9 | end_line_num = line_num 10 | end_line_num = int(end_line_num) 11 | actual_line = [] 12 | lines = '' 13 | with open(file_path, encoding='utf-8') as f: 14 | r = range(max(0, line_num - 10), line_num + 10) 15 | for i, line in enumerate(f): 16 | if i in r: 17 | lines += line 18 | if i + 1 in range(line_num, end_line_num + 1): 19 | actual_line.append(line) 20 | code = lines.split('\n') 21 | return actual_line, code 22 | 23 | 24 | def _code_context(file_path, line_num, end_line_num=None, prefix=''): 25 | actual_line, code = _code(file_path, line_num, end_line_num) 26 | return { 27 | prefix + 'code': code, 28 | prefix + 'file_path': file_path, 29 | prefix + 'line_num': line_num, 30 | prefix + 'actual_line': actual_line 31 | } 32 | 33 | 34 | def _code_context_from_request(request, end_line_num=None, prefix=''): 35 | file_path = request.GET.get('file_path') 36 | line_num = request.GET.get('line_num') 37 | result = {} 38 | if file_path is not None and line_num is not None: 39 | result = _code_context(file_path, line_num, end_line_num, prefix) 40 | return result 41 | 42 | 43 | def _should_display_file_name(file_name): 44 | for ignored_file in SilkyConfig().SILKY_IGNORE_FILES: 45 | if ignored_file in file_name: 46 | return False 47 | return True 48 | -------------------------------------------------------------------------------- /silk/templates/silk/base/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | {% block pagetitle %}Silky{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% block style %} 13 | {% endblock %} 14 | 15 | 16 | 17 | 18 | {% block js %} 19 | {% endblock %} 20 | 21 | 22 | 23 | 24 | {% block top %} 25 | 26 | {% endblock %} 27 |
28 | 38 |
39 | {% block data %} 40 | {% endblock %} 41 |
42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /silk/views/profile_detail.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.utils.decorators import method_decorator 3 | from django.views.generic import View 4 | 5 | from silk.auth import login_possibly_required, permissions_possibly_required 6 | from silk.models import Profile 7 | from silk.views.code import _code_context, _code_context_from_request 8 | 9 | 10 | class ProfilingDetailView(View): 11 | 12 | @method_decorator(login_possibly_required) 13 | @method_decorator(permissions_possibly_required) 14 | def get(self, request, *_, **kwargs): 15 | profile_id = kwargs['profile_id'] 16 | context = { 17 | 'request': request 18 | } 19 | profile = Profile.objects.get(pk=profile_id) 20 | file_path = profile.file_path 21 | line_num = profile.line_num 22 | 23 | context['pos'] = pos = int(request.GET.get('pos', 0)) 24 | if pos: 25 | context.update(_code_context_from_request(request, prefix='pyprofile_')) 26 | 27 | context['profile'] = profile 28 | context['line_num'] = file_path 29 | context['file_path'] = line_num 30 | context['file_column'] = 5 31 | 32 | if profile.request: 33 | context['silk_request'] = profile.request 34 | if file_path and line_num: 35 | try: 36 | context.update(_code_context(file_path, line_num, profile.end_line_num)) 37 | except OSError as e: 38 | if e.errno == 2: 39 | context['code_error'] = e.filename + ' does not exist.' 40 | else: 41 | raise e 42 | 43 | return render(request, 'silk/profile_detail.html', context) 44 | -------------------------------------------------------------------------------- /silk/templates/silk/cprofile.html: -------------------------------------------------------------------------------- 1 | {% extends "silk/base/detail_base.html" %} 2 | {% load silk_filters %} 3 | {% load silk_nav %} 4 | {% load silk_inclusion %} 5 | {% load static %} 6 | 7 | {% block pagetitle %}Silky - CProfile - {{ silk_request.path }}{% endblock %} 8 | 9 | {% block js %} 10 | 11 | 12 | {{ block.super }} 13 | {% endblock %} 14 | 15 | {% block style %} 16 | {{ block.super }} 17 | 18 | 19 | {% endblock %} 20 | 21 | {% block menu %} 22 | {% request_menu request silk_request %} 23 | {% endblock %} 24 | 25 | 26 | {% block data %} 27 |
28 |
29 | {% if silk_request.pyprofile %} 30 |
31 |
32 |
CProfile
33 |
34 |
35 | The below is a dump from the cPython profiler. 36 |
37 | {% if silk_request.prof_file %} 38 | Click here to download profile. 39 | {% endif %} 40 |
{{ silk_request.pyprofile }}
41 | {% endif %} 42 |
43 |
44 |
45 | 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /project/tests/test_config_meta.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import NonCallableMock 2 | 3 | from django.test import TestCase 4 | 5 | from silk.collector import DataCollector 6 | from silk.config import SilkyConfig 7 | from silk.middleware import SilkyMiddleware 8 | from silk.models import Request 9 | 10 | from .util import delete_all_models 11 | 12 | 13 | def fake_get_response(): 14 | def fake_response(): 15 | return 'hello world' 16 | return fake_response 17 | 18 | 19 | class TestConfigMeta(TestCase): 20 | def _mock_response(self): 21 | response = NonCallableMock() 22 | response.headers = {} 23 | response.status_code = 200 24 | response.queries = [] 25 | response.get = response.headers.get 26 | response.content = '' 27 | return response 28 | 29 | def _execute_request(self): 30 | delete_all_models(Request) 31 | DataCollector().configure(Request.objects.create()) 32 | response = self._mock_response() 33 | SilkyMiddleware(fake_get_response)._process_response('', response) 34 | self.assertTrue(response.status_code == 200) 35 | objs = Request.objects.all() 36 | self.assertEqual(objs.count(), 1) 37 | r = objs[0] 38 | return r 39 | 40 | def test_enabled(self): 41 | SilkyConfig().SILKY_META = True 42 | r = self._execute_request() 43 | self.assertTrue(r.meta_time is not None 44 | or r.meta_num_queries is not None 45 | or r.meta_time_spent_queries is not None) 46 | 47 | def test_disabled(self): 48 | SilkyConfig().SILKY_META = False 49 | r = self._execute_request() 50 | self.assertFalse(r.meta_time) 51 | -------------------------------------------------------------------------------- /silk/templates/silk/clear_db.html: -------------------------------------------------------------------------------- 1 | {% extends 'silk/base/root_base.html' %} 2 | {% load silk_inclusion %} 3 | {% load static %} 4 | {% block pagetitle %}Silky - Clear DB{% endblock %} 5 | 6 | {% block menu %} 7 | {% root_menu request %} 8 | {% endblock %} 9 | 10 | {% block style %} 11 | {{ block.super }} 12 | 13 | {% endblock %} 14 | 15 | {% block js %} 16 | {{ block.super }} 17 | 18 | {% endblock %} 19 | {% block data %} 20 |
21 |
22 |

Silk Clear DB

23 |
24 | {% csrf_token %} 25 |
26 | 30 | 34 | 38 |
39 | 40 |
41 |
{{ msg|linebreaks }}
42 |
43 |
44 | 45 | {% endblock %} 46 | 47 | {# Hide filter hamburger menu #} 48 | {% block top %}{% endblock %} 49 | {% block filter %}{% endblock %} 50 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | 5 | # allow setup.py to be run from any path 6 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 7 | 8 | setup( 9 | name='django-silk', 10 | use_scm_version=True, 11 | packages=['silk'], 12 | include_package_data=True, 13 | license='MIT License', 14 | description='Silky smooth profiling for the Django Framework', 15 | long_description=open('README.md').read(), 16 | long_description_content_type='text/markdown', 17 | url='https://github.com/jazzband/django-silk', 18 | author='Michael Ford', 19 | author_email='mtford@gmail.com', 20 | classifiers=[ 21 | 'Development Status :: 5 - Production/Stable', 22 | 'Environment :: Web Environment', 23 | 'Framework :: Django', 24 | 'Framework :: Django :: 4.2', 25 | 'Framework :: Django :: 5.1', 26 | 'Framework :: Django :: 5.2', 27 | 'Framework :: Django :: 6.0', 28 | 'Intended Audience :: Developers', 29 | 'Operating System :: OS Independent', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 3.10', 32 | 'Programming Language :: Python :: 3.11', 33 | 'Programming Language :: Python :: 3.12', 34 | 'Programming Language :: Python :: 3.13', 35 | 'Programming Language :: Python :: 3.14', 36 | 'Topic :: Internet :: WWW/HTTP', 37 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 38 | ], 39 | install_requires=[ 40 | 'Django>=4.2', 41 | 'sqlparse', 42 | 'gprof2dot>=2017.09.19', 43 | ], 44 | extras_require={ 45 | 'formatting': ['autopep8'], 46 | }, 47 | python_requires='>=3.10', 48 | setup_requires=['setuptools_scm'], 49 | ) 50 | -------------------------------------------------------------------------------- /silk/templates/silk/inclusion/profile_menu.html: -------------------------------------------------------------------------------- 1 | {% load silk_nav %} 2 | 9 | 14 | 15 | 33 | 52 | -------------------------------------------------------------------------------- /silk/views/clear_db.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from django.db import transaction 5 | from django.shortcuts import render 6 | from django.utils.decorators import method_decorator 7 | from django.views.generic import View 8 | 9 | from silk.auth import login_possibly_required, permissions_possibly_required 10 | from silk.config import SilkyConfig 11 | from silk.models import Profile, Request, Response, SQLQuery 12 | from silk.utils.data_deletion import delete_model 13 | 14 | 15 | @method_decorator(transaction.non_atomic_requests, name="dispatch") 16 | class ClearDBView(View): 17 | 18 | @method_decorator(login_possibly_required) 19 | @method_decorator(permissions_possibly_required) 20 | def get(self, request, *_, **kwargs): 21 | return render(request, 'silk/clear_db.html') 22 | 23 | @method_decorator(login_possibly_required) 24 | @method_decorator(permissions_possibly_required) 25 | def post(self, request, *_, **kwargs): 26 | context = {} 27 | if 'clear_all' in request.POST: 28 | delete_model(Profile) 29 | delete_model(SQLQuery) 30 | delete_model(Response) 31 | delete_model(Request) 32 | tables = ['Response', 'SQLQuery', 'Profile', 'Request'] 33 | context['msg'] = 'Cleared data for following silk tables: {}'.format(', '.join(tables)) 34 | 35 | if SilkyConfig().SILKY_DELETE_PROFILES: 36 | dir = SilkyConfig().SILKY_PYTHON_PROFILER_RESULT_PATH 37 | for files in os.listdir(dir): 38 | path = os.path.join(dir, files) 39 | try: 40 | shutil.rmtree(path) 41 | except OSError: 42 | os.remove(path) 43 | context['msg'] += '\nDeleted all profiles from the directory.' 44 | 45 | return render(request, 'silk/clear_db.html', context=context) 46 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | Silk is installed like any other Django app. 5 | 6 | First install via pip: 7 | 8 | .. code-block:: bash 9 | 10 | pip install django-silk 11 | 12 | Add the following to your ``settings.py``: 13 | 14 | .. code-block:: python 15 | 16 | MIDDLEWARE = [ 17 | ... 18 | 'silk.middleware.SilkyMiddleware', 19 | ... 20 | ] 21 | 22 | TEMPLATES = [{ 23 | ... 24 | 'OPTIONS': { 25 | 'context_processors': [ 26 | ... 27 | 'django.template.context_processors.request', 28 | ], 29 | }, 30 | }] 31 | 32 | 33 | INSTALLED_APPS = [ 34 | ... 35 | 'silk.apps.SilkAppConfig' 36 | ] 37 | 38 | Add the following to your ``urls.py``: 39 | 40 | .. code-block:: python 41 | 42 | urlpatterns += [path('silk', include('silk.urls', namespace='silk'))] 43 | 44 | Run ``migrate`` to create Silk's database tables: 45 | 46 | .. code-block:: bash 47 | 48 | python manage.py migrate 49 | 50 | And voila! Silk will begin intercepting requests and queries which you can inspect by visiting ``/silk/`` 51 | 52 | Python Snippet Formatting 53 | ------------------------- 54 | 55 | Silk supports generating Python snippets to reproduce requests. 56 | To enable autopep8 formatting of these snippets, install Silk with the `formatting` extras: 57 | 58 | .. code-block:: bash 59 | 60 | pip install django-silk[formatting] 61 | 62 | Other Installation Options 63 | -------------------------- 64 | 65 | You can download a release from `github `_ and then install using pip: 66 | 67 | .. code-block:: bash 68 | 69 | pip install django-silk-.tar.gz 70 | 71 | You can also install directly from the github repo but please note that this version is not guaranteed to be working: 72 | 73 | .. code-block:: bash 74 | 75 | pip install -e git+https://github.com/jazzband/django-silk.git#egg=django_silk 76 | -------------------------------------------------------------------------------- /silk/static/silk/css/components/fonts.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Fira Sans 3 | */ 4 | @font-face { 5 | font-family: FiraSans; 6 | src: url(../../fonts/fira/FiraSans-Regular.woff); 7 | font-weight: normal; 8 | } 9 | @font-face { 10 | font-family: FiraSans; 11 | src: url(../../fonts/fira/FiraSans-Medium.woff); 12 | font-weight: bold; 13 | } 14 | @font-face { 15 | font-family: FiraSans; 16 | src: url(../../fonts/fira/FiraSans-Bold.woff); 17 | font-weight: bolder; 18 | } 19 | @font-face { 20 | font-family: FiraSans; 21 | src: url(../../fonts/fira/FiraSans-Light.woff); 22 | font-weight: lighter; 23 | } 24 | @font-face { 25 | font-family: FiraSans; 26 | src: url(../../fonts/fira/FiraSans-RegularItalic.woff); 27 | font-weight: normal; 28 | font-style: italic; 29 | } 30 | @font-face { 31 | font-family: FiraSans; 32 | src: url(../../fonts/fira/FiraSans-MediumItalic.woff); 33 | font-weight: bold; 34 | font-style: italic; 35 | } 36 | @font-face { 37 | font-family: FiraSans; 38 | src: url(../../fonts/fira/FiraSans-BoldItalic.woff); 39 | font-weight: bolder; 40 | font-style: italic; 41 | } 42 | @font-face { 43 | font-family: FiraSans; 44 | src: url(../../fonts/fira/FiraSans-LightItalic.woff); 45 | font-weight: lighter; 46 | font-style: italic; 47 | } 48 | /** 49 | * Fantasque 50 | */ 51 | @font-face { 52 | font-family: Fantasque; 53 | src: url(../../fonts/fantasque/FantasqueSansMono-Regular.woff); 54 | font-weight: normal; 55 | } 56 | @font-face { 57 | font-family: Fantasque; 58 | src: url(../../fonts/fantasque/FantasqueSansMono-Bold.woff); 59 | font-weight: bold; 60 | } 61 | @font-face { 62 | font-family: Fantasque; 63 | src: url(../../fonts/fantasque/FantasqueSansMono-RegItalic.woff); 64 | font-weight: normal; 65 | font-style: italic; 66 | } 67 | @font-face { 68 | font-family: Fantasque; 69 | src: url(../../fonts/fantasque/FantasqueSansMono-BoldItalic.woff); 70 | font-weight: bold; 71 | font-style: italic; 72 | } 73 | -------------------------------------------------------------------------------- /silk/views/sql.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.utils.decorators import method_decorator 3 | from django.views.generic import View 4 | 5 | from silk.auth import login_possibly_required, permissions_possibly_required 6 | from silk.models import Profile, Request, SQLQuery 7 | from silk.utils.pagination import _page 8 | 9 | __author__ = 'mtford' 10 | 11 | 12 | class SQLView(View): 13 | page_sizes = [5, 10, 25, 100, 200, 500, 1000] 14 | default_page_size = 200 15 | 16 | @method_decorator(login_possibly_required) 17 | @method_decorator(permissions_possibly_required) 18 | def get(self, request, *_, **kwargs): 19 | request_id = kwargs.get('request_id') 20 | profile_id = kwargs.get('profile_id') 21 | try: 22 | per_page = int(request.GET.get('per_page', self.default_page_size)) 23 | except (TypeError, ValueError): 24 | per_page = self.default_page_size 25 | context = { 26 | 'request': request, 27 | 'options_page_size': self.page_sizes, 28 | 'per_page': per_page, 29 | } 30 | if request_id: 31 | silk_request = Request.objects.get(id=request_id) 32 | query_set = SQLQuery.objects.filter(request=silk_request).order_by('-start_time') 33 | for q in query_set: 34 | q.start_time_relative = q.start_time - silk_request.start_time 35 | page = _page(request, query_set, per_page) 36 | context['silk_request'] = silk_request 37 | if profile_id: 38 | p = Profile.objects.get(id=profile_id) 39 | page = _page(request, p.queries.order_by('-start_time').all(), per_page) 40 | context['profile'] = p 41 | if not (request_id or profile_id): 42 | raise KeyError('no profile_id or request_id') 43 | # noinspection PyUnboundLocalVariable 44 | context['items'] = page 45 | return render(request, 'silk/sql.html', context) 46 | -------------------------------------------------------------------------------- /scss/components/fonts.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Fira Sans 3 | */ 4 | 5 | @font-face { 6 | font-family: FiraSans; 7 | src: url(../../fonts/fira/FiraSans-Regular.woff); 8 | font-weight: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: FiraSans; 13 | src: url(../../fonts/fira/FiraSans-Medium.woff); 14 | font-weight: bold; 15 | } 16 | 17 | @font-face { 18 | font-family: FiraSans; 19 | src: url(../../fonts/fira/FiraSans-Bold.woff); 20 | font-weight: bolder; 21 | } 22 | 23 | @font-face { 24 | font-family: FiraSans; 25 | src: url(../../fonts/fira/FiraSans-Light.woff); 26 | font-weight: lighter; 27 | } 28 | 29 | @font-face { 30 | font-family: FiraSans; 31 | src: url(../../fonts/fira/FiraSans-RegularItalic.woff); 32 | font-weight: normal; 33 | font-style: italic; 34 | } 35 | 36 | @font-face { 37 | font-family: FiraSans; 38 | src: url(../../fonts/fira/FiraSans-MediumItalic.woff); 39 | font-weight: bold; 40 | font-style: italic; 41 | } 42 | 43 | @font-face { 44 | font-family: FiraSans; 45 | src: url(../../fonts/fira/FiraSans-BoldItalic.woff); 46 | font-weight: bolder; 47 | font-style: italic; 48 | } 49 | 50 | @font-face { 51 | font-family: FiraSans; 52 | src: url(../../fonts/fira/FiraSans-LightItalic.woff); 53 | font-weight: lighter; 54 | font-style: italic; 55 | } 56 | 57 | /** 58 | * Fantasque 59 | */ 60 | 61 | @font-face { 62 | font-family: Fantasque; 63 | src: url(../../fonts/fantasque/FantasqueSansMono-Regular.woff); 64 | font-weight: normal; 65 | } 66 | 67 | @font-face { 68 | font-family: Fantasque; 69 | src: url(../../fonts/fantasque/FantasqueSansMono-Bold.woff); 70 | font-weight: bold; 71 | } 72 | 73 | @font-face { 74 | font-family: Fantasque; 75 | src: url(../../fonts/fantasque/FantasqueSansMono-RegItalic.woff); 76 | font-weight: normal; 77 | font-style: italic; 78 | } 79 | 80 | @font-face { 81 | font-family: Fantasque; 82 | src: url(../../fonts/fantasque/FantasqueSansMono-BoldItalic.woff); 83 | font-weight: bold; 84 | font-style: italic; 85 | } 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | .eggs 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .pytest_cache/ 33 | .coverage 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | 38 | # Translations 39 | *.mo 40 | 41 | # Mr Developer 42 | .mr.developer.cfg 43 | .project 44 | .pydevproject 45 | 46 | # Rope 47 | .ropeproject 48 | 49 | # Django stuff: 50 | *.log 51 | *.pot 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # Other 57 | dist 58 | .idea 59 | *db.sqlite* 60 | /django_silky/media 61 | *.prof 62 | project/media/ 63 | project/tmp/ 64 | .vscode/ 65 | 66 | # Hardlinks 67 | /django_silky/silk 68 | 69 | # Pip 70 | /src 71 | 72 | # Sphinx 73 | _html 74 | 75 | # Tox 76 | .tox.ini.swp 77 | 78 | # Node 79 | node_modules 80 | 81 | 82 | # Gulp 83 | .gulp-scss-cache 84 | .sass-cache 85 | 86 | *~ 87 | .DS_Store 88 | 89 | ### PyCharm ### 90 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 91 | *.iml 92 | 93 | ## Directory-based project format: 94 | .idea/ 95 | 96 | ## File-based project format: 97 | *.ipr 98 | *.iws 99 | 100 | ## Plugin-specific files: 101 | 102 | # IntelliJ 103 | /out/ 104 | 105 | # mpeltonen/sbt-idea plugin 106 | .idea_modules/ 107 | 108 | # JIRA plugin 109 | atlassian-ide-plugin.xml 110 | 111 | # Crashlytics plugin (for Android Studio and IntelliJ) 112 | com_crashlytics_export_strings.xml 113 | crashlytics.properties 114 | crashlytics-build.properties 115 | fabric.properties 116 | 117 | # Virtual env 118 | .venv* 119 | 120 | package-lock.json 121 | *.db 122 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: 'v6.0.0' 4 | hooks: 5 | - id: check-merge-conflict 6 | - repo: https://github.com/hadialqattan/pycln 7 | rev: v2.6.0 8 | hooks: 9 | - id: pycln 10 | args: ['--all'] 11 | - repo: https://github.com/asottile/yesqa 12 | rev: v1.5.0 13 | hooks: 14 | - id: yesqa 15 | - repo: https://github.com/pycqa/isort 16 | rev: '7.0.0' 17 | hooks: 18 | - id: isort 19 | args: ['--profile', 'black'] 20 | - repo: https://github.com/pre-commit/pre-commit-hooks 21 | rev: 'v6.0.0' 22 | hooks: 23 | - id: end-of-file-fixer 24 | exclude: >- 25 | ^docs/[^/]*\.svg$ 26 | - id: requirements-txt-fixer 27 | - id: trailing-whitespace 28 | types: [python] 29 | - id: file-contents-sorter 30 | files: | 31 | CONTRIBUTORS.txt| 32 | docs/spelling_wordlist.txt| 33 | .gitignore| 34 | .gitattributes 35 | - id: check-case-conflict 36 | - id: check-json 37 | - id: check-xml 38 | - id: check-toml 39 | - id: check-xml 40 | - id: check-yaml 41 | - id: debug-statements 42 | - id: check-added-large-files 43 | - id: check-symlinks 44 | - id: debug-statements 45 | - id: detect-aws-credentials 46 | args: ['--allow-missing-credentials'] 47 | - id: detect-private-key 48 | exclude: ^examples|(?:tests/ssl)/ 49 | - repo: https://github.com/asottile/pyupgrade 50 | rev: 'v3.21.2' 51 | hooks: 52 | - id: pyupgrade 53 | args: ['--keep-mock'] 54 | - repo: https://github.com/adamchainz/django-upgrade 55 | rev: '1.29.1' 56 | hooks: 57 | - id: django-upgrade 58 | args: [--target-version, '4.2'] 59 | - repo: https://github.com/hhatto/autopep8 60 | rev: 'v2.3.2' 61 | hooks: 62 | - id: autopep8 63 | - repo: https://github.com/PyCQA/flake8 64 | rev: '7.3.0' 65 | hooks: 66 | - id: flake8 67 | exclude: '^docs/' 68 | - repo: https://github.com/Lucas-C/pre-commit-hooks-markup 69 | rev: v1.0.1 70 | hooks: 71 | - id: rst-linter 72 | files: >- 73 | ^[^/]+[.]rst$ 74 | -------------------------------------------------------------------------------- /project/tests/test_profile_parser.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import cProfile 3 | import io 4 | import re 5 | 6 | from django.test import TestCase 7 | 8 | from silk.utils.profile_parser import parse_profile 9 | 10 | 11 | class ProfileParserTestCase(TestCase): 12 | def test_profile_parser(self): 13 | """ 14 | Verify that the function parse_profile produces the expected output. 15 | """ 16 | with contextlib.closing(io.StringIO()) as stream: 17 | with contextlib.redirect_stdout(stream): 18 | cProfile.run('print()') 19 | stream.seek(0) 20 | actual = list(parse_profile(stream)) 21 | 22 | # Expected format for the profiling output on cPython implementations (PyPy differs) 23 | # actual = [ 24 | # ["ncalls", "tottime", "percall", "cumtime", "percall", "filename:lineno(function)"], 25 | # ["1", "0.000", "0.000", "0.000", "0.000", ":1()"], 26 | # ["1", "0.000", "0.000", "0.000", "0.000", "{built-in method builtins.exec}"], 27 | # ["1", "0.000", "0.000", "0.000", "0.000", "{built-in method builtins.print}"], 28 | # ["1", "0.000", "0.000", "0.000", "0.000", "{method 'disable' of '_lsprof.Profiler' objects}"], 29 | # ] 30 | 31 | exc_header = ["ncalls", "tottime", "percall", "cumtime", "percall", "filename:lineno(function)"] 32 | self.assertEqual(actual[0], exc_header) 33 | 34 | exc_number = re.compile(r"\d(.\d+)?") 35 | exc_module = re.compile(r"({method.*})|({built-in.*})|(<.+>:\d+\(<.+>\))") 36 | 37 | exc_row = [exc_number, exc_number, exc_number, exc_number, exc_number, exc_module] 38 | 39 | for row in actual[1:]: 40 | for text, expected_regex in zip(row, exc_row): 41 | self.assertRegex( 42 | text, expected_regex, 43 | msg="Expected something like {} but found {}" 44 | ) 45 | -------------------------------------------------------------------------------- /silk/code_generation/django_test_client.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlencode 2 | 3 | try: 4 | import autopep8 5 | except ImportError: 6 | autopep8 = None 7 | from django.template import Context, Template 8 | 9 | from silk.profiling.dynamic import is_str_typ 10 | 11 | template = """\ 12 | from django.test import Client 13 | c = Client() 14 | response = c.{{ lower_case_method }}(path='{{ path }}'{% if data or content_type %},{% else %}){% endif %}{% if data %} 15 | data={{ data }}{% endif %}{% if data and content_type %},{% elif data %}){% endif %}{% if content_type %} 16 | content_type='{{ content_type }}'){% endif %} 17 | """ 18 | 19 | 20 | def _encode_query_params(query_params): 21 | try: 22 | query_params = urlencode(query_params) 23 | except TypeError: 24 | pass 25 | return '?' + query_params 26 | 27 | 28 | def gen(path, method=None, query_params=None, data=None, content_type=None): 29 | # generates python code representing a call via django client. 30 | # useful for use in testing 31 | method = method.lower() 32 | t = Template(template) 33 | context = { 34 | 'path': path, 35 | 'lower_case_method': method, 36 | 'content_type': content_type, 37 | } 38 | if method == 'get': 39 | context['data'] = query_params 40 | else: 41 | if query_params: 42 | query_params = _encode_query_params(query_params) 43 | path += query_params 44 | if is_str_typ(data): 45 | data = "'%s'" % data 46 | context['data'] = data 47 | context['query_params'] = query_params 48 | code = t.render(Context(context, autoescape=False)) 49 | if autopep8: 50 | # autopep8 is not a hard requirement, so we check if it's available 51 | # if autopep8 is available, we use it to format the code and do things 52 | # like remove long lines and improve readability 53 | code = autopep8.fix_code( 54 | code, 55 | options=autopep8.parse_args(['--aggressive', '']), 56 | ) 57 | return code 58 | -------------------------------------------------------------------------------- /silk/views/request_detail.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.shortcuts import render 4 | from django.utils.decorators import method_decorator 5 | from django.views.generic import View 6 | 7 | from silk.auth import login_possibly_required, permissions_possibly_required 8 | from silk.code_generation.curl import curl_cmd 9 | from silk.code_generation.django_test_client import gen 10 | from silk.models import Request 11 | 12 | 13 | class RequestView(View): 14 | 15 | @method_decorator(login_possibly_required) 16 | @method_decorator(permissions_possibly_required) 17 | def get(self, request, request_id): 18 | silk_request = Request.objects.get(pk=request_id) 19 | query_params = None 20 | if silk_request.query_params: 21 | query_params = json.loads(silk_request.query_params) 22 | 23 | context = { 24 | 'silk_request': silk_request, 25 | 'query_params': json.dumps(query_params, sort_keys=True, indent=4) if query_params else None, 26 | 'request': request 27 | } 28 | 29 | if len(silk_request.raw_body) < 20000: # Don't do this for large request 30 | body = silk_request.raw_body 31 | try: 32 | body = json.loads(body) # Incase encoded as JSON 33 | except (ValueError, TypeError): 34 | pass 35 | context['curl'] = curl_cmd(url=request.build_absolute_uri(silk_request.path), 36 | method=silk_request.method, 37 | query_params=query_params, 38 | body=body, 39 | content_type=silk_request.content_type) 40 | context['client'] = gen(path=silk_request.path, 41 | method=silk_request.method, 42 | query_params=query_params, 43 | data=body, 44 | content_type=silk_request.content_type) 45 | 46 | return render(request, 'silk/request.html', context) 47 | -------------------------------------------------------------------------------- /silk/config.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | 3 | from silk.singleton import Singleton 4 | 5 | 6 | def default_permissions(user): 7 | if user: 8 | return user.is_staff 9 | return False 10 | 11 | 12 | class SilkyConfig(metaclass=Singleton): 13 | defaults = { 14 | 'SILKY_DYNAMIC_PROFILING': [], 15 | 'SILKY_IGNORE_PATHS': [], 16 | 'SILKY_HIDE_COOKIES': True, 17 | 'SILKY_IGNORE_QUERIES': [], 18 | 'SILKY_META': False, 19 | 'SILKY_AUTHENTICATION': False, 20 | 'SILKY_AUTHORISATION': False, 21 | 'SILKY_PERMISSIONS': default_permissions, 22 | 'SILKY_MAX_RECORDED_REQUESTS': 10**4, 23 | 'SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT': 10, 24 | 'SILKY_MAX_REQUEST_BODY_SIZE': -1, 25 | 'SILKY_MAX_RESPONSE_BODY_SIZE': -1, 26 | 'SILKY_INTERCEPT_PERCENT': 100, 27 | 'SILKY_INTERCEPT_FUNC': None, 28 | 'SILKY_PYTHON_PROFILER': False, 29 | 'SILKY_PYTHON_PROFILER_FUNC': None, 30 | 'SILKY_STORAGE_CLASS': 'silk.storage.ProfilerResultStorage', 31 | 'SILKY_PYTHON_PROFILER_EXTENDED_FILE_NAME': False, 32 | 'SILKY_MIDDLEWARE_CLASS': 'silk.middleware.SilkyMiddleware', 33 | 'SILKY_JSON_ENSURE_ASCII': True, 34 | 'SILKY_ANALYZE_QUERIES': False, 35 | 'SILKY_EXPLAIN_FLAGS': None, 36 | 'SILKY_SENSITIVE_KEYS': {'username', 'api', 'token', 'key', 'secret', 'password', 'signature'}, 37 | 'SILKY_DELETE_PROFILES': False 38 | } 39 | 40 | def _setup(self): 41 | from django.conf import settings 42 | 43 | options = {option: getattr(settings, option) for option in dir(settings) if option.startswith('SILKY')} 44 | self.attrs = copy(self.defaults) 45 | self.attrs['SILKY_PYTHON_PROFILER_RESULT_PATH'] = settings.MEDIA_ROOT 46 | self.attrs.update(options) 47 | 48 | def __init__(self): 49 | super().__init__() 50 | self._setup() 51 | 52 | def __getattr__(self, item): 53 | return self.attrs.get(item, None) 54 | 55 | def __setattribute__(self, key, value): 56 | self.attrs[key] = value 57 | -------------------------------------------------------------------------------- /project/tests/test_code.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | from django.test import TestCase 4 | 5 | from silk.views.code import _code, _code_context, _code_context_from_request 6 | 7 | FILE_PATH = __file__ 8 | LINE_NUM = 5 9 | END_LINE_NUM = 10 10 | 11 | with open(__file__) as f: 12 | ACTUAL_LINES = [line + '\n' for line in f.read().split('\n')] 13 | 14 | 15 | class CodeTestCase(TestCase): 16 | 17 | def assertActualLineEqual(self, actual_line, end_line_num=None): 18 | expected_actual_line = ACTUAL_LINES[LINE_NUM - 1:end_line_num or LINE_NUM] 19 | self.assertEqual(actual_line, expected_actual_line) 20 | 21 | def assertCodeEqual(self, code): 22 | expected_code = [line.strip('\n') for line in ACTUAL_LINES[0:LINE_NUM + 10]] + [''] 23 | self.assertEqual(code, expected_code) 24 | 25 | def test_code(self): 26 | for end_line_num in None, END_LINE_NUM: 27 | actual_line, code = _code(FILE_PATH, LINE_NUM, end_line_num) 28 | self.assertActualLineEqual(actual_line, end_line_num) 29 | self.assertCodeEqual(code) 30 | 31 | def test_code_context(self): 32 | for end_line_num in None, END_LINE_NUM: 33 | for prefix in '', 'salchicha_': 34 | context = _code_context(FILE_PATH, LINE_NUM, end_line_num, prefix) 35 | self.assertActualLineEqual(context[prefix + 'actual_line'], end_line_num) 36 | self.assertCodeEqual(context[prefix + 'code']) 37 | self.assertEqual(context[prefix + 'file_path'], FILE_PATH) 38 | self.assertEqual(context[prefix + 'line_num'], LINE_NUM) 39 | 40 | def test_code_context_from_request(self): 41 | for end_line_num in None, END_LINE_NUM: 42 | for prefix in '', 'salchicha_': 43 | request = namedtuple('Request', 'GET')(dict(file_path=FILE_PATH, line_num=LINE_NUM)) 44 | context = _code_context_from_request(request, end_line_num, prefix) 45 | self.assertActualLineEqual(context[prefix + 'actual_line'], end_line_num) 46 | self.assertCodeEqual(context[prefix + 'code']) 47 | self.assertEqual(context[prefix + 'file_path'], FILE_PATH) 48 | self.assertEqual(context[prefix + 'line_num'], LINE_NUM) 49 | -------------------------------------------------------------------------------- /silk/static/silk/lib/highlight/foundation.css: -------------------------------------------------------------------------------- 1 | /* 2 | Description: Foundation 4 docs style for highlight.js 3 | Author: Dan Allen 4 | Website: http://foundation.zurb.com/docs/ 5 | Version: 1.0 6 | Date: 2013-04-02 7 | */ 8 | 9 | .hljs { 10 | display: block; padding: 0.5em; 11 | background: #eee; 12 | } 13 | 14 | .hljs-header, 15 | .hljs-decorator, 16 | .hljs-annotation { 17 | color: #000077; 18 | } 19 | 20 | .hljs-horizontal_rule, 21 | .hljs-link_url, 22 | .hljs-emphasis, 23 | .hljs-attribute { 24 | color: #070; 25 | } 26 | 27 | .hljs-emphasis { 28 | font-style: italic; 29 | } 30 | 31 | .hljs-link_label, 32 | .hljs-strong, 33 | .hljs-value, 34 | .hljs-string, 35 | .scss .hljs-value .hljs-string { 36 | color: #d14; 37 | } 38 | 39 | .hljs-strong { 40 | font-weight: bold; 41 | } 42 | 43 | .hljs-blockquote, 44 | .hljs-comment { 45 | color: #998; 46 | font-style: italic; 47 | } 48 | 49 | .asciidoc .hljs-title, 50 | .hljs-function .hljs-title { 51 | color: #900; 52 | } 53 | 54 | .hljs-class { 55 | color: #458; 56 | } 57 | 58 | .hljs-id, 59 | .hljs-pseudo, 60 | .hljs-constant, 61 | .hljs-hexcolor { 62 | color: teal; 63 | } 64 | 65 | .hljs-variable { 66 | color: #336699; 67 | } 68 | 69 | .hljs-bullet, 70 | .hljs-javadoc { 71 | color: #997700; 72 | } 73 | 74 | .hljs-pi, 75 | .hljs-doctype { 76 | color: #3344bb; 77 | } 78 | 79 | .hljs-code, 80 | .hljs-number { 81 | color: #099; 82 | } 83 | 84 | .hljs-important { 85 | color: #f00; 86 | } 87 | 88 | .smartquote, 89 | .hljs-label { 90 | color: #970; 91 | } 92 | 93 | .hljs-preprocessor, 94 | .hljs-pragma { 95 | color: #579; 96 | } 97 | 98 | .hljs-reserved, 99 | .hljs-keyword, 100 | .scss .hljs-value { 101 | color: #000; 102 | } 103 | 104 | .hljs-regexp { 105 | background-color: #fff0ff; 106 | color: #880088; 107 | } 108 | 109 | .hljs-symbol { 110 | color: #990073; 111 | } 112 | 113 | .hljs-symbol .hljs-string { 114 | color: #a60; 115 | } 116 | 117 | .hljs-tag { 118 | color: #007700; 119 | } 120 | 121 | .hljs-at_rule, 122 | .hljs-at_rule .hljs-keyword { 123 | color: #088; 124 | } 125 | 126 | .hljs-at_rule .hljs-preprocessor { 127 | color: #808; 128 | } 129 | 130 | .scss .hljs-tag, 131 | .scss .hljs-attribute { 132 | color: #339; 133 | } 134 | -------------------------------------------------------------------------------- /silk/static/silk/css/pages/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: FiraSans, "Helvetica Neue", Arial, sans-serif; 3 | background-color: #f3f3f3; 4 | margin: 0; 5 | font-weight: lighter; 6 | } 7 | 8 | pre { 9 | font-family: Fantasque; 10 | background-color: white !important; 11 | padding: 0.5em !important; 12 | margin: 0 !important; 13 | font-size: 14px; 14 | text-align: left; 15 | } 16 | 17 | code { 18 | font-family: Fantasque; 19 | background-color: white !important; 20 | padding: 0 !important; 21 | margin: 0 !important; 22 | font-size: 14px; 23 | } 24 | 25 | html { 26 | margin: 0; 27 | } 28 | 29 | #header { 30 | height: 50px; 31 | background-color: rgb(51, 51, 68); 32 | width: 100%; 33 | position: relative; 34 | padding: 0; 35 | } 36 | 37 | #header div { 38 | display: inline-block; 39 | } 40 | 41 | .menu { 42 | height: 50px; 43 | padding: 0; 44 | margin: 0; 45 | } 46 | 47 | .menu-item { 48 | height: 50px; 49 | padding-left: 10px; 50 | padding-right: 10px; 51 | margin: 0; 52 | margin-right: -4px; 53 | color: white; 54 | } 55 | 56 | .menu-item a { 57 | color: white !important; 58 | } 59 | 60 | #filter .menu-item { 61 | margin-right: 0px; 62 | } 63 | 64 | .selectable-menu-item { 65 | transition: background-color 0.15s ease, color 0.15s ease; 66 | } 67 | 68 | .selectable-menu-item:hover { 69 | background-color: #f3f3f3; 70 | cursor: pointer; 71 | color: black !important; 72 | } 73 | 74 | .selectable-menu-item:hover a { 75 | color: black !important; 76 | } 77 | 78 | .menu-item-selected { 79 | background-color: #f3f3f3; 80 | color: black !important; 81 | } 82 | 83 | .menu-item-selected a { 84 | color: black !important; 85 | } 86 | 87 | .menu-item-outer { 88 | display: table !important; 89 | height: 100%; 90 | width: 100%; 91 | } 92 | 93 | .menu-item-inner { 94 | display: table-cell !important; 95 | vertical-align: middle; 96 | width: 100%; 97 | } 98 | 99 | a:visited { 100 | color: black; 101 | } 102 | 103 | a { 104 | color: black; 105 | text-decoration: none; 106 | } 107 | 108 | #filter { 109 | height: 50px; 110 | position: absolute; 111 | right: 0; 112 | } 113 | 114 | .description { 115 | font-style: italic; 116 | font-size: 14px; 117 | margin-bottom: 5px; 118 | } 119 | -------------------------------------------------------------------------------- /scss/pages/base.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: FiraSans, "Helvetica Neue", Arial, sans-serif; 3 | background-color: #f3f3f3; 4 | margin: 0; 5 | font-weight: lighter; 6 | } 7 | 8 | pre { 9 | font-family: Fantasque; 10 | background-color: white !important; 11 | padding: 0.5em !important; 12 | margin: 0 !important; 13 | font-size: 14px; 14 | text-align: left; 15 | } 16 | 17 | code { 18 | font-family: Fantasque; 19 | background-color: white !important; 20 | padding: 0 !important; 21 | margin: 0 !important; 22 | font-size: 14px; 23 | 24 | } 25 | 26 | html { 27 | margin: 0; 28 | } 29 | 30 | #header { 31 | height: 50px; 32 | background-color: rgb(51, 51, 68); 33 | width: 100%; 34 | position: relative; 35 | padding: 0; 36 | 37 | 38 | } 39 | 40 | #header div { 41 | display: inline-block; 42 | } 43 | 44 | .menu { 45 | height: 50px; 46 | padding: 0; 47 | margin: 0; 48 | 49 | } 50 | 51 | .menu-item { 52 | height: 50px; 53 | padding-left: 10px; 54 | padding-right: 10px; 55 | margin: 0; 56 | margin-right: -4px; 57 | color: white; 58 | 59 | } 60 | 61 | .menu-item a { 62 | color: white !important; 63 | } 64 | 65 | #filter .menu-item { 66 | margin-right: 0px; 67 | } 68 | 69 | .selectable-menu-item { 70 | transition: background-color 0.15s ease, color 0.15s ease; 71 | 72 | } 73 | 74 | .selectable-menu-item:hover { 75 | background-color: #f3f3f3; 76 | cursor: pointer; 77 | color: black !important; 78 | } 79 | 80 | .selectable-menu-item:hover a { 81 | color: black !important; 82 | } 83 | 84 | .menu-item-selected { 85 | background-color: #f3f3f3; 86 | color: black !important; 87 | } 88 | 89 | .menu-item-selected a { 90 | color: black !important; 91 | } 92 | 93 | .menu-item-outer { 94 | display: table !important; 95 | height: 100%; 96 | width: 100%; 97 | } 98 | 99 | .menu-item-inner { 100 | display: table-cell !important; 101 | vertical-align: middle; 102 | width: 100%; 103 | } 104 | 105 | a:visited { 106 | color: black; 107 | } 108 | 109 | a { 110 | color: black; 111 | text-decoration: none; 112 | } 113 | 114 | #filter { 115 | height: 50px; 116 | position: absolute; 117 | right: 0; 118 | } 119 | 120 | .description { 121 | font-style: italic; 122 | font-size: 14px; 123 | margin-bottom: 5px; 124 | } 125 | -------------------------------------------------------------------------------- /silk/templates/silk/base/root_base.html: -------------------------------------------------------------------------------- 1 | {% extends "silk/base/base.html" %} 2 | {% load silk_nav %} 3 | {% load silk_inclusion %} 4 | {% load static %} 5 | 6 | {% block body_class %} 7 | cbp-spmenu-push 8 | {% endblock %} 9 | 10 | {% block style %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% endblock %} 21 | 22 | {% block filter %} 23 | 30 | {% endblock %} 31 | 32 | {% block top %} 33 | 50 | 51 | {% endblock %} 52 | 53 | {% block js %} 54 | 55 | 56 | 57 | 58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | Authentication and Authorisation 5 | -------------------------------- 6 | 7 | By default anybody can access the Silk user interface by heading to `/silk/`. To enable your Django 8 | auth backend place the following in `settings.py`: 9 | 10 | 11 | .. code-block:: python 12 | 13 | SILKY_AUTHENTICATION = True # User must login 14 | SILKY_AUTHORISATION = True # User must have permissions 15 | 16 | If ``SILKY_AUTHORISATION`` is ``True``, by default Silk will only authorise users with ``is_staff`` attribute set to ``True``. 17 | 18 | You can customise this using the following in ``settings.py``: 19 | 20 | .. code-block:: python 21 | 22 | def my_custom_perms(user): 23 | return user.is_allowed_to_use_silk 24 | 25 | SILKY_PERMISSIONS = my_custom_perms 26 | 27 | 28 | Request and Response bodies 29 | --------------------------- 30 | 31 | By default, Silk will save down the request and response bodies for each request for future viewing 32 | no matter how large. If Silk is used in production under heavy volume with large bodies this can have 33 | a huge impact on space/time performance. This behaviour can be configured with following options: 34 | 35 | .. code-block:: python 36 | 37 | SILKY_MAX_REQUEST_BODY_SIZE = -1 # Silk takes anything <0 as no limit 38 | SILKY_MAX_RESPONSE_BODY_SIZE = 1024 # If response body>1024kb, ignore 39 | 40 | 41 | Meta-Profiling 42 | -------------- 43 | 44 | Sometimes its useful to be able to see what effect Silk is having on the request/response time. To do this add 45 | the following to your `settings.py`: 46 | 47 | .. code-block:: python 48 | 49 | SILKY_META = True 50 | 51 | Silk will then record how long it takes to save everything down to the database at the end of each request: 52 | 53 | .. image:: /images/meta.png 54 | 55 | Note that in the above screenshot, this means that the request took 29ms (22ms from Django and 7ms from Silk) 56 | 57 | Limiting request and response data 58 | ---------------------------------- 59 | 60 | To make sure silky garbage collects old request/response data, a config var can be set to limit the number of request/response rows it stores. 61 | 62 | .. code-block:: python 63 | 64 | SILKY_MAX_RECORDED_REQUESTS = 10**4 65 | 66 | The garbage collection is only run on a percentage of requests to reduce overhead. It can be adjusted with this config: 67 | 68 | .. code-block:: python 69 | 70 | SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = 10 71 | -------------------------------------------------------------------------------- /project/tests/test_db.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test profiling of DB queries without mocking, to catch possible 3 | incompatibility 4 | """ 5 | from django.shortcuts import reverse 6 | from django.test import Client, TestCase 7 | 8 | from silk.collector import DataCollector 9 | from silk.config import SilkyConfig 10 | from silk.models import Request 11 | from silk.profiling.profiler import silk_profile 12 | 13 | from .factories import BlindFactory 14 | 15 | 16 | class TestDbQueries(TestCase): 17 | @classmethod 18 | def setUpClass(cls): 19 | super().setUpClass() 20 | BlindFactory.create_batch(size=5) 21 | SilkyConfig().SILKY_META = False 22 | 23 | def test_profile_request_to_db(self): 24 | DataCollector().configure(Request(reverse('example_app:index'))) 25 | 26 | with silk_profile(name='test_profile'): 27 | resp = self.client.get(reverse('example_app:index')) 28 | 29 | DataCollector().profiles.values() 30 | assert len(resp.context['blinds']) == 5 31 | 32 | def test_profile_request_to_db_with_constraints(self): 33 | DataCollector().configure(Request(reverse('example_app:create'))) 34 | 35 | resp = self.client.post(reverse('example_app:create'), {'name': 'Foo'}) 36 | self.assertEqual(resp.status_code, 302) 37 | 38 | 39 | class TestAnalyzeQueries(TestCase): 40 | 41 | @classmethod 42 | def setUpClass(cls): 43 | super().setUpClass() 44 | BlindFactory.create_batch(size=5) 45 | SilkyConfig().SILKY_META = False 46 | SilkyConfig().SILKY_ANALYZE_QUERIES = True 47 | 48 | @classmethod 49 | def tearDownClass(cls): 50 | super().tearDownClass() 51 | SilkyConfig().SILKLY_ANALYZE_QUERIES = False 52 | 53 | def test_analyze_queries(self): 54 | DataCollector().configure(Request(reverse('example_app:index'))) 55 | client = Client() 56 | 57 | with silk_profile(name='test_profile'): 58 | resp = client.get(reverse('example_app:index')) 59 | 60 | DataCollector().profiles.values() 61 | assert len(resp.context['blinds']) == 5 62 | 63 | 64 | class TestAnalyzeQueriesExplainParams(TestAnalyzeQueries): 65 | 66 | @classmethod 67 | def setUpClass(cls): 68 | super().setUpClass() 69 | SilkyConfig().SILKY_EXPLAIN_FLAGS = {'verbose': True} 70 | 71 | @classmethod 72 | def tearDownClass(cls): 73 | super().tearDownClass() 74 | SilkyConfig().SILKY_EXPLAIN_FLAGS = None 75 | -------------------------------------------------------------------------------- /silk/code_generation/curl.py: -------------------------------------------------------------------------------- 1 | import json 2 | from urllib.parse import urlencode 3 | 4 | from django.template import Context, Template 5 | 6 | curl_template = """\ 7 | curl {% if method %}-X {{ method }}{% endif %} 8 | {% if content_type %}-H 'content-type: {{ content_type }}'{% endif %} 9 | {% if modifier %}{{ modifier }} {% endif %}{% if body %}'{{ body }}'{% endif %} 10 | {{ url }}{% if query_params %}{{ query_params }}{% endif %} 11 | {% if extra %}{{ extra }}{% endif %} 12 | """ 13 | 14 | 15 | def _curl_process_params(body, content_type, query_params): 16 | extra = None 17 | if query_params: 18 | try: 19 | query_params = urlencode( 20 | [(k, v.encode('utf8')) for k, v in query_params.items()] 21 | ) 22 | except TypeError: 23 | pass 24 | query_params = '?' + str(query_params) 25 | if 'json' in content_type or 'javascript' in content_type: 26 | if isinstance(body, dict): 27 | body = json.dumps(body) 28 | modifier = '-d' 29 | # See http://curl.haxx.se/docs/manpage.html#-F 30 | # for multipart vs x-www-form-urlencoded 31 | # x-www-form-urlencoded is same way as browser, 32 | # multipart is RFC 2388 which allows file uploads. 33 | elif 'multipart' in content_type or 'x-www-form-urlencoded' in content_type: 34 | try: 35 | body = ' '.join([f'{k}={v}' for k, v in body.items()]) 36 | except AttributeError: 37 | modifier = '-d' 38 | else: 39 | content_type = None 40 | modifier = '-F' 41 | elif body: 42 | body = str(body) 43 | modifier = '-d' 44 | else: 45 | modifier = None 46 | content_type = None 47 | # TODO: Clean up. 48 | return modifier, body, query_params, content_type, extra 49 | 50 | 51 | def curl_cmd(url, method=None, query_params=None, body=None, content_type=None): 52 | if not content_type: 53 | content_type = 'text/plain' 54 | modifier, body, query_params, content_type, extra = _curl_process_params( 55 | body, 56 | content_type, 57 | query_params, 58 | ) 59 | t = Template(curl_template) 60 | context = { 61 | 'url': url, 62 | 'method': method, 63 | 'query_params': query_params, 64 | 'body': body, 65 | 'modifier': modifier, 66 | 'content_type': content_type, 67 | 'extra': extra, 68 | } 69 | return t.render(Context(context, autoescape=False)).replace('\n', ' ') 70 | -------------------------------------------------------------------------------- /silk/views/profile_dot.py: -------------------------------------------------------------------------------- 1 | # std 2 | import json 3 | import os 4 | import shutil 5 | import tempfile 6 | from contextlib import closing, contextmanager 7 | 8 | # 3rd party 9 | from io import StringIO 10 | 11 | from django.http import HttpResponse 12 | from django.shortcuts import get_object_or_404 13 | from django.utils.decorators import method_decorator 14 | from django.views.generic import View 15 | from gprof2dot import DotWriter, PstatsParser, Theme 16 | 17 | # silk 18 | from silk.auth import login_possibly_required, permissions_possibly_required 19 | from silk.models import Request 20 | 21 | COLOR_MAP = Theme( 22 | mincolor=(0.18, 0.51, 0.53), 23 | maxcolor=(0.03, 0.49, 0.50), 24 | gamma=1.5, 25 | fontname='FiraSans', 26 | minfontsize=6.0, 27 | maxfontsize=6.0, 28 | ) 29 | 30 | 31 | @contextmanager 32 | def _temp_file_from_file_field(source): 33 | """ 34 | Create a temp file containing data from a django file field. 35 | """ 36 | source.open() 37 | with closing(source): 38 | try: 39 | with tempfile.NamedTemporaryFile(delete=False) as destination: 40 | shutil.copyfileobj(source, destination) 41 | yield destination.name 42 | finally: 43 | os.unlink(destination.name) 44 | 45 | 46 | def _create_profile(source, get_filename=_temp_file_from_file_field): 47 | """ 48 | Parse a profile from a django file field source. 49 | """ 50 | with get_filename(source) as filename: 51 | return PstatsParser(filename).parse() 52 | 53 | 54 | def _create_dot(profile, cutoff): 55 | """ 56 | Create a dot file from pstats data stored in a django file field. 57 | """ 58 | node_cutoff = cutoff / 100.0 59 | edge_cutoff = 0.1 / 100.0 60 | profile.prune(node_cutoff, edge_cutoff, [], False) 61 | 62 | with closing(StringIO()) as fp: 63 | DotWriter(fp).graph(profile, COLOR_MAP) 64 | return fp.getvalue() 65 | 66 | 67 | class ProfileDotView(View): 68 | 69 | @method_decorator(login_possibly_required) 70 | @method_decorator(permissions_possibly_required) 71 | def get(self, request, request_id): 72 | silk_request = get_object_or_404(Request, pk=request_id, prof_file__isnull=False) 73 | cutoff = float(request.GET.get('cutoff', '') or 5) 74 | profile = _create_profile(silk_request.prof_file) 75 | result = dict(dot=_create_dot(profile, cutoff)) 76 | return HttpResponse(json.dumps(result).encode('utf-8'), content_type='application/json') 77 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /silk/templatetags/silk_filters.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.template import Library 4 | from django.template.defaultfilters import stringfilter 5 | from django.utils import timezone 6 | from django.utils.html import conditional_escape 7 | from django.utils.safestring import mark_safe 8 | 9 | register = Library() 10 | 11 | 12 | def _no_op(x): 13 | return x 14 | 15 | 16 | def _esc_func(autoescape): 17 | if autoescape: 18 | return conditional_escape 19 | return _no_op 20 | 21 | 22 | @stringfilter 23 | def spacify(value, autoescape=None): 24 | esc = _esc_func(autoescape) 25 | val = esc(value).replace(' ', " ") 26 | val = val.replace('\t', ' ') 27 | return mark_safe(val) 28 | 29 | 30 | def _urlify(str): 31 | r = re.compile(r'"(?P.*\.py)", line (?P[0-9]+).*') 32 | m = r.search(str) 33 | while m: 34 | group = m.groupdict() 35 | src = group['src'] 36 | num = group['num'] 37 | start = m.start('src') 38 | end = m.end('src') 39 | rep = '{src}'.format(src=src, num=num) 40 | str = str[:start] + rep + str[end:] 41 | m = r.search(str) 42 | return str 43 | 44 | 45 | @register.filter 46 | def hash(h, key): 47 | return h[key] 48 | 49 | 50 | def _process_microseconds(dt_strftime): 51 | splt = dt_strftime.split('.') 52 | micro = splt[-1] 53 | time = '.'.join(splt[0:-1]) 54 | micro = '%.3f' % float('0.' + micro) 55 | return time + micro[1:] 56 | 57 | 58 | def _silk_date_time(dt): 59 | today = timezone.now().date() 60 | if dt.date() == today: 61 | dt_strftime = dt.strftime('%H:%M:%S.%f') 62 | return _process_microseconds(dt_strftime) 63 | else: 64 | return _process_microseconds(dt.strftime('%Y.%m.%d %H:%M.%f')) 65 | 66 | 67 | @register.filter(expects_localtime=True) 68 | def silk_date_time(dt): 69 | return _silk_date_time(dt) 70 | 71 | 72 | @register.filter 73 | def sorted(value): 74 | return sorted(value) 75 | 76 | 77 | @stringfilter 78 | def filepath_urlify(value, autoescape=None): 79 | value = _urlify(value) 80 | return mark_safe(value) 81 | 82 | 83 | @stringfilter 84 | def body_filter(value): 85 | print(value) 86 | if len(value) > 20: 87 | return 'Too big!' 88 | else: 89 | return value 90 | 91 | 92 | spacify.needs_autoescape = True 93 | filepath_urlify.needs_autoescape = True 94 | register.filter(spacify) 95 | register.filter(filepath_urlify) 96 | register.filter(body_filter) 97 | -------------------------------------------------------------------------------- /docs/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | Troubleshooting 2 | =============== 3 | 4 | The below details common problems when using Silk, most of which have been derived from the solutions to github issues. 5 | 6 | Unicode 7 | ------- 8 | 9 | Silk saves down the request and response bodies of each HTTP request by default. These bodies are often UTF encoded and hence it is important that Silk's database tables are also UTF encoded. Django has no facility for enforcing this and instead assumes that the configured database defaults to UTF. 10 | 11 | If you see errors like: 12 | 13 | 14 | Incorrect string value: '\xCE\xBB, \xCF\x86...' for column 'raw_body' at row... 15 | 16 | 17 | Then it's likely your database is not configured correctly for UTF encoding. 18 | 19 | See this `github issue `_ for more details and workarounds. 20 | 21 | Context Processor 22 | ----------------- 23 | 24 | Silk requires the template context to include a ``request`` object in order to save and analyze it. 25 | 26 | If you see errors like: 27 | 28 | .. code-block:: text 29 | 30 | File "/service/venv/lib/python3.12/site-packages/silk/templatetags/silk_nav.py", line 9, in navactive 31 | path = request.path 32 | ^^^^^^^^^^^^ 33 | AttributeError: 'str' object has no attribute 'path' 34 | 35 | Include ``django.template.context_processors.request`` in your Django settings' ``TEMPLATES`` context processors as `recommended `_. 36 | 37 | Middleware 38 | ---------- 39 | 40 | The order of middleware is sensitive. If any middleware placed before ``silk.middleware.SilkyMiddleware`` returns a response without invoking its ``get_response``, the ``SilkyMiddleware`` won’t run. To avoid this, ensure that middleware preceding ``SilkyMiddleware`` does not bypass or return a response without calling its ``get_response``. For further details, check out the `Django documentation `. 41 | 42 | Garbage Collection 43 | ------------------ 44 | 45 | To `avoid `_ `deadlock `_ `issues `_, you might want to decouple silk's garbage collection from your webserver's request processing, set ``SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT=0`` and trigger it manually, e.g. in a cron job: 46 | 47 | .. code-block:: bash 48 | 49 | python manage.py silk_request_garbage_collect 50 | -------------------------------------------------------------------------------- /project/tests/test_config_max_body_size.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | from django.test import TestCase 4 | from django.urls import reverse 5 | 6 | from silk.collector import DataCollector 7 | from silk.config import SilkyConfig 8 | from silk.model_factory import RequestModelFactory, ResponseModelFactory 9 | from silk.models import Request 10 | 11 | 12 | class TestMaxBodySizeRequest(TestCase): 13 | 14 | def test_no_max_request(self): 15 | SilkyConfig().SILKY_MAX_REQUEST_BODY_SIZE = -1 16 | mock_request = Mock() 17 | mock_request.headers = {'content-type': 'text/plain'} 18 | mock_request.GET = {} 19 | mock_request.path = reverse('silk:requests') 20 | mock_request.method = 'get' 21 | mock_request.body = b'a' * 1000 # 1000 bytes? 22 | request_model = RequestModelFactory(mock_request).construct_request_model() 23 | self.assertTrue(request_model.raw_body) 24 | 25 | def test_max_request(self): 26 | SilkyConfig().SILKY_MAX_REQUEST_BODY_SIZE = 10 # 10kb 27 | mock_request = Mock() 28 | mock_request.headers = {'content-type': 'text/plain'} 29 | mock_request.GET = {} 30 | mock_request.method = 'get' 31 | mock_request.body = b'a' * 1024 * 100 # 100kb 32 | mock_request.path = reverse('silk:requests') 33 | request_model = RequestModelFactory(mock_request).construct_request_model() 34 | self.assertFalse(request_model.raw_body) 35 | 36 | 37 | class TestMaxBodySizeResponse(TestCase): 38 | 39 | def setUp(self): 40 | DataCollector().request = Request.objects.create() 41 | 42 | def test_no_max_response(self): 43 | SilkyConfig().SILKY_MAX_RESPONSE_BODY_SIZE = -1 44 | mock_response = Mock() 45 | mock_response.headers = {'content-type': 'text/plain'} 46 | mock_response.content = b'a' * 1000 # 1000 bytes? 47 | mock_response.status_code = 200 48 | mock_response.get = mock_response.headers.get 49 | response_model = ResponseModelFactory(mock_response).construct_response_model() 50 | self.assertTrue(response_model.raw_body) 51 | 52 | def test_max_response(self): 53 | SilkyConfig().SILKY_MAX_RESPONSE_BODY_SIZE = 10 # 10kb 54 | mock_response = Mock() 55 | mock_response.headers = {'content-type': 'text/plain'} 56 | mock_response.content = b'a' * 1024 * 100 # 100kb 57 | mock_response.status_code = 200 58 | mock_response.get = mock_response.headers.get 59 | response_model = ResponseModelFactory(mock_response).construct_response_model() 60 | self.assertFalse(response_model.raw_body) 61 | -------------------------------------------------------------------------------- /project/tests/test_config_auth.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TestCase 3 | from django.urls import NoReverseMatch, reverse 4 | 5 | from silk.config import SilkyConfig, default_permissions 6 | from silk.middleware import silky_reverse 7 | 8 | 9 | class TestAuth(TestCase): 10 | def test_authentication(self): 11 | SilkyConfig().SILKY_AUTHENTICATION = True 12 | response = self.client.get(silky_reverse('requests')) 13 | self.assertEqual(response.status_code, 302) 14 | url = response.url 15 | try: 16 | # If we run tests within the django_silk project, a login url is available from example_app 17 | self.assertIn(reverse('login'), url) 18 | except NoReverseMatch: 19 | # Otherwise the Django default login url is used, in which case we can test for that instead 20 | self.assertIn('http://testserver/login/', url) 21 | 22 | def test_default_authorisation(self): 23 | SilkyConfig().SILKY_AUTHENTICATION = True 24 | SilkyConfig().SILKY_AUTHORISATION = True 25 | SilkyConfig().SILKY_PERMISSIONS = default_permissions 26 | username_and_password = 'bob' # bob is an imbecile and uses the same pass as his username 27 | user = User.objects.create(username=username_and_password) 28 | user.set_password(username_and_password) 29 | user.save() 30 | self.client.login(username=username_and_password, password=username_and_password) 31 | response = self.client.get(silky_reverse('requests')) 32 | self.assertEqual(response.status_code, 403) 33 | user.is_staff = True 34 | user.save() 35 | response = self.client.get(silky_reverse('requests')) 36 | self.assertEqual(response.status_code, 200) 37 | 38 | def test_custom_authorisation(self): 39 | SilkyConfig().SILKY_AUTHENTICATION = True 40 | SilkyConfig().SILKY_AUTHORISATION = True 41 | 42 | def custom_authorisation(user): 43 | return user.username.startswith('mike') 44 | 45 | SilkyConfig().SILKY_PERMISSIONS = custom_authorisation 46 | username_and_password = 'bob' # bob is an imbecile and uses the same pass as his username 47 | user = User.objects.create(username=username_and_password) 48 | user.set_password(username_and_password) 49 | user.save() 50 | self.client.login(username=username_and_password, password=username_and_password) 51 | response = self.client.get(silky_reverse('requests')) 52 | self.assertEqual(response.status_code, 403) 53 | user.username = 'mike2' 54 | user.save() 55 | response = self.client.get(silky_reverse('requests')) 56 | self.assertEqual(response.status_code, 200) 57 | -------------------------------------------------------------------------------- /silk/views/sql_detail.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from django.core.exceptions import PermissionDenied 5 | from django.shortcuts import render 6 | from django.utils.decorators import method_decorator 7 | from django.utils.safestring import mark_safe 8 | from django.views.generic import View 9 | 10 | from silk.auth import login_possibly_required, permissions_possibly_required 11 | from silk.models import Profile, Request, SQLQuery 12 | from silk.views.code import _code 13 | 14 | 15 | class SQLDetailView(View): 16 | def _urlify(self, str): 17 | files = [] 18 | r = re.compile(r'"(?P.*\.py)", line (?P[0-9]+).*') 19 | m = r.search(str) 20 | n = 1 21 | while m: 22 | group = m.groupdict() 23 | src = group['src'] 24 | files.append(src) 25 | num = group['num'] 26 | start = m.start('src') 27 | end = m.end('src') 28 | rep = '{src}'.format( 29 | pos=n, 30 | src=src, 31 | num=num, 32 | name='c%d' % n, 33 | ) 34 | str = str[:start] + rep + str[end:] 35 | m = r.search(str) 36 | n += 1 37 | return str, files 38 | 39 | @method_decorator(login_possibly_required) 40 | @method_decorator(permissions_possibly_required) 41 | def get(self, request, *_, **kwargs): 42 | sql_id = kwargs.get('sql_id', None) 43 | request_id = kwargs.get('request_id', None) 44 | profile_id = kwargs.get('profile_id', None) 45 | sql_query = SQLQuery.objects.get(pk=sql_id) 46 | pos = int(request.GET.get('pos', 0)) 47 | file_path = request.GET.get('file_path', '') 48 | line_num = int(request.GET.get('line_num', 0)) 49 | tb = sql_query.traceback_ln_only 50 | analysis = sql_query.analysis 51 | str, files = self._urlify(tb) 52 | if file_path and file_path not in files: 53 | raise PermissionDenied 54 | tb = [mark_safe(x) for x in str.split('\n')] 55 | context = { 56 | 'sql_query': sql_query, 57 | 'traceback': tb, 58 | 'pos': pos, 59 | 'line_num': line_num, 60 | 'file_path': file_path, 61 | 'analysis': analysis, 62 | 'virtualenv_path': os.environ.get('VIRTUAL_ENV') or '', 63 | } 64 | if request_id: 65 | context['silk_request'] = Request.objects.get(pk=request_id) 66 | if profile_id: 67 | context['profile'] = Profile.objects.get(pk=int(profile_id)) 68 | if pos and file_path and line_num: 69 | actual_line, code = _code(file_path, line_num) 70 | context['code'] = code 71 | context['actual_line'] = actual_line 72 | return render(request, 'silk/sql_detail.html', context) 73 | -------------------------------------------------------------------------------- /silk/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from silk.views.clear_db import ClearDBView 4 | from silk.views.cprofile import CProfileView 5 | from silk.views.profile_detail import ProfilingDetailView 6 | from silk.views.profile_dot import ProfileDotView 7 | from silk.views.profile_download import ProfileDownloadView 8 | from silk.views.profiling import ProfilingView 9 | from silk.views.raw import Raw 10 | from silk.views.request_detail import RequestView 11 | from silk.views.requests import RequestsView 12 | from silk.views.sql import SQLView 13 | from silk.views.sql_detail import SQLDetailView 14 | from silk.views.summary import SummaryView 15 | 16 | app_name = 'silk' 17 | urlpatterns = [ 18 | path(route='', view=SummaryView.as_view(), name='summary'), 19 | path(route='requests/', view=RequestsView.as_view(), name='requests'), 20 | path( 21 | route='request//', 22 | view=RequestView.as_view(), 23 | name='request_detail', 24 | ), 25 | path( 26 | route='request//sql/', 27 | view=SQLView.as_view(), 28 | name='request_sql', 29 | ), 30 | path( 31 | route='request//sql//', 32 | view=SQLDetailView.as_view(), 33 | name='request_sql_detail', 34 | ), 35 | path( 36 | route='request//raw/', 37 | view=Raw.as_view(), 38 | name='raw', 39 | ), 40 | path( 41 | route='request//pyprofile/', 42 | view=ProfileDownloadView.as_view(), 43 | name='request_profile_download', 44 | ), 45 | path( 46 | route='request//json/', 47 | view=ProfileDotView.as_view(), 48 | name='request_profile_dot', 49 | ), 50 | path( 51 | route='request//profiling/', 52 | view=ProfilingView.as_view(), 53 | name='request_profiling', 54 | ), 55 | path( 56 | route='request//profile//', 57 | view=ProfilingDetailView.as_view(), 58 | name='request_profile_detail', 59 | ), 60 | path( 61 | route='request//profile//sql/', 62 | view=SQLView.as_view(), 63 | name='request_and_profile_sql', 64 | ), 65 | path( 66 | route='request//profile//sql//', 67 | view=SQLDetailView.as_view(), 68 | name='request_and_profile_sql_detail', 69 | ), 70 | path( 71 | route='profile//', 72 | view=ProfilingDetailView.as_view(), 73 | name='profile_detail', 74 | ), 75 | path( 76 | route='profile//sql/', 77 | view=SQLView.as_view(), 78 | name='profile_sql', 79 | ), 80 | path( 81 | route='profile//sql//', 82 | view=SQLDetailView.as_view(), 83 | name='profile_sql_detail', 84 | ), 85 | path(route='profiling/', view=ProfilingView.as_view(), name='profiling'), 86 | path(route='cleardb/', view=ClearDBView.as_view(), name='cleardb'), 87 | path( 88 | route='request//cprofile/', 89 | view=CProfileView.as_view(), 90 | name='cprofile', 91 | ), 92 | ] 93 | -------------------------------------------------------------------------------- /silk/templates/silk/sql_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "silk/base/detail_base.html" %} 2 | {% load static %} 3 | {% load silk_filters %} 4 | {% load silk_nav %} 5 | {% load silk_inclusion %} 6 | 7 | {% block pagetitle %}Silky - SQL Detail - {{ silk_request.path }}{% endblock %} 8 | 9 | {% block js %} 10 | {{ block.super }} 11 | 12 | {% endblock %} 13 | 14 | {% block style %} 15 | {{ block.super }} 16 | 17 | {% endblock %} 18 | 19 | {% block menu %} 20 | 21 | 30 | 35 | 36 | 37 | 42 | {% endblock %} 43 | 44 | {% block data %} 45 |
46 |
47 |
{{ sql_query.formatted_query|spacify|linebreaksbr }}
48 |
49 |
50 | {{ sql_query.time_taken }}ms 51 |
52 |
53 | {{ sql_query.num_joins }} joins 54 |
55 |
56 | 57 |
58 | {% if analysis %} 59 |
60 |
61 |
62 | Query Plan 63 |
64 |
65 | {{ analysis | spacify | linebreaksbr }} 66 |
67 | {% endif %} 68 |
69 |
70 |
71 | Traceback 72 |
73 |
74 |
75 | The below is the Python stacktrace that leads up the execution of the above SQL query. 76 | Use it to figure out where and why this SQL query is being executed and whether or not 77 | it's actually neccessary. 78 |
79 | {% for ln in traceback %} 80 | {% if ln %} 81 |
82 | {{ ln }} 83 |
84 | {% if forloop.counter == pos %} 85 | {% code code actual_line %} 86 | {% endif %} 87 | {% endif %} 88 | {% endfor %} 89 |
90 |
91 | 92 | 93 | {% endblock %} 94 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: build (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}) 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] 13 | django-version: ['4.2', '5.1', '5.2', '6.0', 'main'] 14 | postgres-version: ['14', '18'] 15 | mariadb-version: ['10.6', '10.11', '11.4', '11.8'] 16 | exclude: 17 | # Django 4.2 doesn't support Python >= 3.13 18 | - django-version: '4.2' 19 | python-version: '3.13' 20 | - django-version: '4.2' 21 | python-version: '3.14' 22 | 23 | # Django 5.1 doesn't support Python >= 3.14 24 | - django-version: '5.1' 25 | python-version: '3.14' 26 | 27 | # Django 6.0 doesn't support Python <3.12 (https://docs.djangoproject.com/en/dev/releases/6.0/#python-compatibility) 28 | - django-version: '6.0' 29 | python-version: '3.10' 30 | - django-version: '6.0' 31 | python-version: '3.11' 32 | - django-version: 'main' 33 | python-version: '3.10' 34 | - django-version: 'main' 35 | python-version: '3.11' 36 | 37 | services: 38 | postgres: 39 | image: postgres:${{ matrix.postgres-version }} 40 | env: 41 | POSTGRES_USER: postgres 42 | POSTGRES_PASSWORD: postgres 43 | POSTGRES_DB: postgres 44 | ports: 45 | - 5432:5432 46 | options: >- 47 | --health-cmd pg_isready 48 | --health-interval 10s 49 | --health-timeout 5s 50 | --health-retries 5 51 | 52 | mariadb: 53 | image: mariadb:${{ matrix.mariadb-version }} 54 | env: 55 | MYSQL_ROOT_PASSWORD: mysql 56 | MYSQL_DATABASE: mysql 57 | options: >- 58 | --health-cmd "mariadb-admin ping" 59 | --health-interval 10s 60 | --health-timeout 5s 61 | --health-retries 5 62 | ports: 63 | - 3306:3306 64 | 65 | steps: 66 | - uses: actions/checkout@v4 67 | 68 | - name: Set up Python ${{ matrix.python-version }} 69 | uses: actions/setup-python@v5 70 | with: 71 | python-version: ${{ matrix.python-version }} 72 | 73 | - name: Get pip cache dir 74 | id: pip-cache 75 | run: | 76 | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT 77 | 78 | - name: Cache 79 | uses: actions/cache@v4 80 | with: 81 | path: ${{ steps.pip-cache.outputs.dir }} 82 | key: 83 | ${{ matrix.python-version }}-v1-${{ hashFiles('**/requirements.txt') }}-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/tox.ini') }} 84 | restore-keys: | 85 | ${{ matrix.python-version }}-v1- 86 | 87 | - name: Install dependencies 88 | run: | 89 | python -m pip install --upgrade pip 90 | python -m pip install --upgrade tox tox-gh-actions 91 | 92 | - name: Tox tests 93 | run: | 94 | tox -v 95 | env: 96 | DJANGO: ${{ matrix.django-version }} 97 | 98 | - name: Upload coverage 99 | uses: codecov/codecov-action@v3 100 | with: 101 | name: Python ${{ matrix.python-version }} 102 | -------------------------------------------------------------------------------- /docs/profiling.rst: -------------------------------------------------------------------------------- 1 | Profiling 2 | ========= 3 | 4 | Silk can be used to profile arbitrary blocks of code and provides ``silk_profile``, a Python decorator and a context manager for this purpose. Profiles will then appear in the 'Profiling' tab within Silk's user interface. 5 | 6 | Decorator 7 | --------- 8 | 9 | The decorator can be applied to both functions and methods: 10 | 11 | .. code-block:: python 12 | 13 | @silk_profile(name='View Blog Post') 14 | def post(request, post_id): 15 | p = Post.objects.get(pk=post_id) 16 | return render(request, 'post.html', { 17 | 'post': p 18 | }) 19 | 20 | 21 | .. code-block:: python 22 | 23 | class MyView(View): 24 | @silk_profile(name='View Blog Post') 25 | def get(self, request): 26 | p = Post.objects.get(pk=post_id) 27 | return render(request, 'post.html', { 28 | 'post': p 29 | }) 30 | 31 | Context Manager 32 | --------------- 33 | 34 | ``silk_profile`` can also be used as a context manager: 35 | 36 | .. code-block:: python 37 | 38 | def post(request, post_id): 39 | with silk_profile(name='View Blog Post #%d' % self.pk): 40 | p = Post.objects.get(pk=post_id) 41 | return render(request, 'post.html', { 42 | 'post': p 43 | }) 44 | 45 | Dynamic Profiling 46 | ----------------- 47 | 48 | Decorators and context managers can also be injected at run-time. This is useful if we want to narrow down slow requests/database queries to dependencies. 49 | 50 | Dynamic profiling is configured via the ``SILKY_DYNAMIC_PROFILING`` option in your ``settings.py``: 51 | 52 | .. code-block:: python 53 | 54 | """ 55 | Dynamic function decorator 56 | """ 57 | 58 | SILKY_DYNAMIC_PROFILING = [{ 59 | 'module': 'path.to.module', 60 | 'function': 'foo' 61 | }] 62 | 63 | # ... is roughly equivalent to 64 | @silk_profile() 65 | def foo(): 66 | pass 67 | 68 | """ 69 | Dynamic method decorator 70 | """ 71 | 72 | SILKY_DYNAMIC_PROFILING = [{ 73 | 'module': 'path.to.module', 74 | 'function': 'MyClass.bar' 75 | }] 76 | 77 | # ... is roughly equivalent to 78 | class MyClass: 79 | 80 | @silk_profile() 81 | def bar(self): 82 | pass 83 | 84 | """ 85 | Dynamic code block profiling 86 | """ 87 | 88 | SILKY_DYNAMIC_PROFILING = [{ 89 | 'module': 'path.to.module', 90 | 'function': 'foo', 91 | # Line numbers are relative to the function as opposed to the file in which it resides 92 | 'start_line': 1, 93 | 'end_line': 2, 94 | 'name': 'Slow Foo' 95 | }] 96 | 97 | # ... is roughly equivalent to 98 | def foo(): 99 | with silk_profile(name='Slow Foo'): 100 | print (1) 101 | print (2) 102 | print(3) 103 | print(4) 104 | 105 | Note that dynamic profiling behaves in a similar fashion to that of the python mock framework in that 106 | we modify the function in-place e.g: 107 | 108 | .. code-block:: python 109 | 110 | """ my.module """ 111 | from another.module import foo 112 | 113 | # ...do some stuff 114 | foo() 115 | # ...do some other stuff 116 | 117 | 118 | We would profile ``foo`` by dynamically decorating `my.module.foo` as opposed to ``another.module.foo``: 119 | 120 | .. code-block:: python 121 | 122 | SILKY_DYNAMIC_PROFILING = [{ 123 | 'module': 'my.module', 124 | 'function': 'foo' 125 | }] 126 | 127 | If we were to apply the dynamic profile to the functions source module ``another.module.foo`` *after* it has already been imported, no profiling would be triggered. 128 | -------------------------------------------------------------------------------- /project/project/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 4 | 5 | SECRET_KEY = 'ey5!m&h-uj6c7dzp@(o1%96okkq4!&bjja%oi*v3r=2t(!$7os' 6 | 7 | DEBUG = True 8 | DEBUG_PROPAGATE_EXCEPTIONS = True 9 | 10 | ALLOWED_HOSTS = [] 11 | 12 | INSTALLED_APPS = ( 13 | 'django.contrib.staticfiles', 14 | 'django.contrib.admin', 15 | 'django.contrib.auth', 16 | 'django.contrib.contenttypes', 17 | 'django.contrib.messages', 18 | 'django.contrib.sessions', 19 | 'silk', 20 | 'example_app' 21 | ) 22 | 23 | ROOT_URLCONF = 'project.urls' 24 | 25 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 26 | 27 | MIDDLEWARE = [ 28 | 'django.contrib.sessions.middleware.SessionMiddleware', 29 | 'django.middleware.common.CommonMiddleware', 30 | 'django.middleware.csrf.CsrfViewMiddleware', 31 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 32 | 'django.contrib.messages.middleware.MessageMiddleware', 33 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 34 | 'silk.middleware.SilkyMiddleware' 35 | ] 36 | 37 | WSGI_APPLICATION = 'wsgi.application' 38 | 39 | DB_ENGINE = os.environ.get("DB_ENGINE", "postgresql") 40 | 41 | DATABASES = { 42 | "default": { 43 | "ENGINE": f"django.db.backends.{DB_ENGINE}", 44 | "NAME": os.environ.get("DB_NAME", "postgres"), 45 | "USER": os.environ.get("DB_USER", 'postgres'), 46 | "PASSWORD": os.environ.get("DB_PASSWORD", "postgres"), 47 | "HOST": os.environ.get("DB_HOST", "127.0.0.1"), 48 | "PORT": os.environ.get("DB_PORT", 5432), 49 | "ATOMIC_REQUESTS": True 50 | }, 51 | } 52 | 53 | LANGUAGE_CODE = 'en-us' 54 | 55 | TIME_ZONE = 'UTC' 56 | 57 | USE_I18N = True 58 | 59 | USE_TZ = True 60 | 61 | LOGGING = { 62 | 'version': 1, 63 | 'formatters': { 64 | 'mosayc': { 65 | 'format': '%(asctime)-15s %(levelname)-7s %(message)s [%(funcName)s (%(filename)s:%(lineno)s)]', 66 | } 67 | }, 68 | 'handlers': { 69 | 'console': { 70 | 'level': 'DEBUG', 71 | 'class': 'logging.StreamHandler', 72 | 'formatter': 'mosayc' 73 | } 74 | }, 75 | 'loggers': { 76 | 'silk': { 77 | 'handlers': ['console'], 78 | 'level': 'DEBUG' 79 | } 80 | }, 81 | } 82 | 83 | STATIC_URL = '/static/' 84 | 85 | STATICFILES_FINDERS = ( 86 | 'django.contrib.staticfiles.finders.FileSystemFinder', 87 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 88 | ) 89 | 90 | TEMP_DIR = os.path.join(BASE_DIR, "tmp") 91 | STATIC_ROOT = os.path.join(TEMP_DIR, "static") 92 | 93 | if not os.path.exists(STATIC_ROOT): 94 | os.makedirs(STATIC_ROOT) 95 | 96 | MEDIA_ROOT = BASE_DIR + '/media/' 97 | MEDIA_URL = '/media/' 98 | 99 | if not os.path.exists(MEDIA_ROOT): 100 | os.mkdir(MEDIA_ROOT) 101 | 102 | TEMPLATES = [ 103 | { 104 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 105 | 'DIRS': [], 106 | 'APP_DIRS': True, 107 | 'OPTIONS': { 108 | 'context_processors': [ 109 | 'django.template.context_processors.request', 110 | ], 111 | }, 112 | }, 113 | ] 114 | 115 | LOGIN_URL = '/login/' 116 | LOGIN_REDIRECT_URL = '/' 117 | 118 | SILKY_META = True 119 | SILKY_PYTHON_PROFILER = True 120 | SILKY_PYTHON_PROFILER_BINARY = True 121 | # Do not garbage collect for tests 122 | SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = 0 123 | # SILKY_AUTHENTICATION = True 124 | # SILKY_AUTHORISATION = True 125 | -------------------------------------------------------------------------------- /project/tests/test_dynamic_profiling.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from django.test import TestCase 4 | 5 | import silk 6 | from silk.profiling.dynamic import ( 7 | _get_module, 8 | _get_parent_module, 9 | profile_function_or_method, 10 | ) 11 | 12 | from .test_lib.assertion import dict_contains 13 | from .util import mock_data_collector 14 | 15 | 16 | class TestGetModule(TestCase): 17 | """test for _get_module""" 18 | 19 | def test_singular(self): 20 | module = _get_module('silk') 21 | self.assertEqual(module.__class__.__name__, 'module') 22 | self.assertEqual('silk', module.__name__) 23 | self.assertTrue(hasattr(module, 'models')) 24 | 25 | def test_dot(self): 26 | module = _get_module('silk.models') 27 | self.assertEqual(module.__class__.__name__, 'module') 28 | self.assertEqual('silk.models', module.__name__) 29 | self.assertTrue(hasattr(module, 'SQLQuery')) 30 | 31 | 32 | class TestGetParentModule(TestCase): 33 | """test for silk.tools._get_parent_module""" 34 | 35 | def test_singular(self): 36 | parent = _get_parent_module(silk) 37 | self.assertIsInstance(parent, dict) 38 | 39 | def test_dot(self): 40 | import silk.utils 41 | 42 | parent = _get_parent_module(silk.utils) 43 | self.assertEqual(parent, silk) 44 | 45 | 46 | class MyClass: 47 | def foo(self): 48 | pass 49 | 50 | 51 | def foo(): 52 | pass 53 | 54 | 55 | def source_file_name(): 56 | file_name = __file__ 57 | if file_name[-1] == 'c': 58 | file_name = file_name[:-1] 59 | return file_name 60 | 61 | 62 | class TestProfileFunction(TestCase): 63 | def test_method_as_str(self): 64 | # noinspection PyShadowingNames 65 | def foo(_): 66 | pass 67 | 68 | # noinspection PyUnresolvedReferences 69 | with patch.object(MyClass, 'foo', foo): 70 | profile_function_or_method('tests.test_dynamic_profiling', 'MyClass.foo', 'test') 71 | dc = mock_data_collector() 72 | with patch('silk.profiling.profiler.DataCollector', return_value=dc) as mock_DataCollector: 73 | MyClass().foo() 74 | self.assertEqual(mock_DataCollector.return_value.register_profile.call_count, 1) 75 | call_args = mock_DataCollector.return_value.register_profile.call_args[0][0] 76 | self.assertTrue(dict_contains({ 77 | 'func_name': foo.__name__, 78 | 'dynamic': True, 79 | 'file_path': source_file_name(), 80 | 'name': 'test', 81 | 'line_num': foo.__code__.co_firstlineno 82 | }, call_args)) 83 | 84 | def test_func_as_str(self): 85 | name = foo.__name__ 86 | line_num = foo.__code__.co_firstlineno 87 | profile_function_or_method('tests.test_dynamic_profiling', 'foo', 'test') 88 | dc = mock_data_collector() 89 | with patch('silk.profiling.profiler.DataCollector', return_value=dc) as mock_DataCollector: 90 | foo() 91 | self.assertEqual(mock_DataCollector.return_value.register_profile.call_count, 1) 92 | call_args = mock_DataCollector.return_value.register_profile.call_args[0][0] 93 | self.assertTrue(dict_contains({ 94 | 'func_name': name, 95 | 'dynamic': True, 96 | 'file_path': source_file_name(), 97 | 'name': 'test', 98 | 'line_num': line_num 99 | }, call_args)) 100 | -------------------------------------------------------------------------------- /project/tests/test_profile_dot.py: -------------------------------------------------------------------------------- 1 | # std 2 | import cProfile 3 | import os 4 | import tempfile 5 | from contextlib import contextmanager 6 | from unittest.mock import MagicMock 7 | 8 | # 3rd party 9 | from django.test import TestCase 10 | from networkx.drawing.nx_pydot import read_dot 11 | 12 | # silk 13 | from silk.views.profile_dot import ( 14 | _create_dot, 15 | _create_profile, 16 | _temp_file_from_file_field, 17 | ) 18 | 19 | 20 | class ProfileDotViewTestCase(TestCase): 21 | 22 | @classmethod 23 | @contextmanager 24 | def _stats_file(cls): 25 | """ 26 | Context manager to create some arbitrary profiling stats in a temp file, returning the filename on enter, 27 | and removing the temp file on exit. 28 | """ 29 | try: 30 | with tempfile.NamedTemporaryFile(delete=False) as stats: 31 | pass 32 | cProfile.run('1+1', stats.name) 33 | yield stats.name 34 | finally: 35 | os.unlink(stats.name) 36 | 37 | @classmethod 38 | @contextmanager 39 | def _stats_data(cls): 40 | """ 41 | Context manager to create some arbitrary profiling stats in a temp file, returning the data on enter, 42 | and removing the temp file on exit. 43 | """ 44 | with cls._stats_file() as filename: 45 | with open(filename, 'rb') as f: 46 | yield f.read() 47 | 48 | @classmethod 49 | def _profile(cls): 50 | """Create some arbitrary profiling stats.""" 51 | with cls._stats_file() as filename: 52 | # create profile - we don't need to convert a django file field to a temp file 53 | # just use the filename of the temp file already created 54 | @contextmanager 55 | def dummy(_): 56 | yield filename 57 | return _create_profile(filename, dummy) 58 | 59 | @classmethod 60 | def _mock_file(cls, data): 61 | """ 62 | Get a mock object that looks like a file but returns data when read is called. 63 | """ 64 | i = [0] 65 | 66 | def read(n): 67 | if not i[0]: 68 | i[0] += 1 69 | return data 70 | 71 | stream = MagicMock() 72 | stream.open = lambda: None 73 | stream.read = read 74 | 75 | return stream 76 | 77 | def test_create_dot(self): 78 | """ 79 | Verify that a dot file is correctly created from pstats data stored in a file field. 80 | """ 81 | with self._stats_file(): 82 | 83 | try: 84 | # create dot 85 | with tempfile.NamedTemporaryFile(delete=False) as dotfile: 86 | dot = _create_dot(self._profile(), 5) 87 | dotfile.write(dot.encode('utf-8')) 88 | 89 | # verify generated dot is valid 90 | G = read_dot(dotfile.name) 91 | self.assertGreater(len(G.nodes()), 0) 92 | 93 | finally: 94 | os.unlink(dotfile.name) 95 | 96 | def test_temp_file_from_file_field(self): 97 | """ 98 | Verify that data held in a file like object is copied to a temp file. 99 | """ 100 | dummy_data = b'dummy data' 101 | stream = self._mock_file(dummy_data) 102 | 103 | with _temp_file_from_file_field(stream) as filename: 104 | with open(filename, 'rb') as f: 105 | self.assertEqual(f.read(), dummy_data) 106 | 107 | # file should have been removed on exit 108 | self.assertFalse(os.path.exists(filename)) 109 | -------------------------------------------------------------------------------- /silk/templates/silk/profile_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "silk/base/detail_base.html" %} 2 | {% load silk_filters %} 3 | {% load silk_nav %} 4 | {% load silk_inclusion %} 5 | {% load static %} 6 | 7 | {% block pagetitle %}Silky - Profile Detail - {{ silk_request.path }}{% endblock %} 8 | 9 | {% block js %} 10 | 11 | 12 | {{ block.super }} 13 | {% endblock %} 14 | 15 | {% block style %} 16 | {{ block.super }} 17 | 18 | 19 | {% endblock %} 20 | 21 | {% block menu %} 22 | {% profile_menu request profile silk_request %} 23 | {% endblock %} 24 | 25 | {% block data %} 26 |
27 |
28 |
29 | {% profile_summary profile %} 30 |
31 |
32 |
33 | {% if profile.file_path and profile.line_num %} 34 | {{ profile.file_path }}:{{ profile.line_num }}{% if profile.end_line_num %}:{{ profile.end_line_num }}{% endif %} 35 | {% else %} 36 | Location 37 | {% endif %} 38 |
39 |
40 |
41 | Below shows where in your code this profile was defined. If your profile was defined dynamically (i.e in your settings.py), 42 | then this will show the range of lines that are covered by the profiling. 43 |
44 | {% if code %} 45 |
{% code code actual_line %}
46 | {% elif code_error %} 47 |
48 | {{ code_error }} 49 |
50 | {% endif %} 51 | 52 | {% if silk_request.prof_file %} 53 |
54 |
Profile graph
55 |
56 |
57 | Below is a graph of the profile, with the nodes coloured by the time taken (red is more time). This should give a good indication of the slowest path through the profiled code.
58 | 59 | Prune nodes taking up less than 60 | 64 | 65 | 66 | % of the total time 67 | 68 |
69 |
70 |
71 | {% url 'silk:request_profile_dot' request_id=silk_request.pk as profile_dot_url %} 72 | {{ profile_dot_url|json_script:'profileDotURL' }} 73 | 74 | {% endif %} 75 | 76 | {% if silk_request.pyprofile %} 77 |
78 |
79 |
Python Profiler
80 |
81 |
82 | The below is a dump from the cPython profiler. 83 |
84 | {% if silk_request.prof_file %} 85 | Click here to download profile. 86 | {% endif %} 87 |
{{ silk_request.pyprofile }}
88 | {% endif %} 89 |
90 | 91 |
92 | 93 | 94 | {% endblock %} 95 | --------------------------------------------------------------------------------