├── 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 | [](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 |
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 | Photo
15 | Name
16 | Child safe?
17 |
18 | {% for blind in blinds %}
19 |
20 | {% if blind.photo %} {% endif %}
21 | {{ blind.name }}
22 | {% if blind.child_safe %}Yes{% else %}No{% endif %}
23 |
24 | {% endfor %}
25 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------