├── .bowerrc ├── .coveragerc ├── .gitignore ├── .pre-commit-config.yaml ├── .prospector.yaml ├── .travis.yml ├── CHANGELOG.rst ├── CONTRIBUTORS.rst ├── Gulpfile.js ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bower.json ├── demo ├── demo │ ├── __init__.py │ ├── settings.py │ ├── templates │ │ ├── 400.html │ │ ├── 403.html │ │ ├── 404.html │ │ ├── 500.html │ │ └── base.html │ ├── urls.py │ └── wsgi.py └── manage.py ├── package.json ├── requirements.txt ├── runtests.py ├── setup.cfg ├── setup.py ├── status ├── __init__.py ├── __main__.py ├── api │ ├── __init__.py │ ├── mixins.py │ ├── urls.py │ └── views.py ├── apps.py ├── commands.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── status.py ├── providers │ ├── __init__.py │ ├── base.py │ ├── celery │ │ ├── __init__.py │ │ ├── health.py │ │ └── stats.py │ ├── django │ │ ├── __init__.py │ │ ├── health.py │ │ └── stats.py │ ├── health.py │ └── stats.py ├── settings.py ├── static │ ├── js │ │ ├── actions │ │ │ └── AppActions.js │ │ ├── components │ │ │ ├── HealthApp.jsx │ │ │ ├── HealthItem.jsx │ │ │ ├── StatsApp.jsx │ │ │ ├── StatsItem.jsx │ │ │ ├── StatsItemKey.jsx │ │ │ ├── StatusApp.jsx │ │ │ └── StatusItem.jsx │ │ ├── constants │ │ │ └── AppConstants.js │ │ ├── dispatcher │ │ │ └── AppDispatcher.js │ │ ├── health.jsx │ │ ├── main.jsx │ │ ├── stats.jsx │ │ └── stores │ │ │ ├── HealthStore.js │ │ │ ├── StatsStore.js │ │ │ └── StatusStore.js │ └── scss │ │ ├── base.scss │ │ └── colors.scss ├── templates │ └── status │ │ ├── base.html │ │ ├── health.html │ │ ├── main.html │ │ └── stats.html ├── urls.py └── utils.py ├── tests ├── __init__.py ├── requirements.txt └── settings.py └── tox.ini /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "status/static/vendor" 3 | } -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | */settings/* 5 | *__init__.py 6 | *urls* 7 | .tox* 8 | *tests* 9 | */migrations/* 10 | *manage.py 11 | *wsgi.py 12 | *celery.py 13 | *apps.py 14 | 15 | [report] 16 | exclude_lines = 17 | # Don't complain if tests don't hit defensive assertion code: 18 | raise AssertionError 19 | raise NotImplementedError 20 | 21 | # Don't complain if non-runnable code isn't run: 22 | if 0: 23 | if __name__ == .__main__.: 24 | 25 | # Don't complain about missing debug-only code: 26 | def __repr__ 27 | if self\.debug 28 | if settings\.DEBUG 29 | 30 | ignore_errors = True 31 | show_missing = True 32 | 33 | [paths] 34 | source = 35 | ./ 36 | 37 | [html] 38 | directory = .coverage_report/html 39 | 40 | [xml] 41 | output = .coverage_report/coverage.xml 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | *dist/ 29 | *vendor/ 30 | status/static/status/ 31 | demo/static/ 32 | ./static/ 33 | node_modules/ 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage_report/ 49 | .coverage.* 50 | .coverage 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *,cover 55 | .unittests_report/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # PyCharm 71 | .idea/ 72 | 73 | # Celery 74 | celerybeat* 75 | celeryev* 76 | *.pid 77 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: git@github.com:pre-commit/pre-commit-hooks 2 | sha: 18d7035de5388cc7775be57f529c154bf541aab9 3 | hooks: 4 | - id: check-added-large-files 5 | - id: check-merge-conflict 6 | - id: check-xml 7 | - id: check-yaml 8 | - id: debug-statements 9 | - id: double-quote-string-fixer 10 | - id: end-of-file-fixer 11 | - id: fix-encoding-pragma 12 | - id: name-tests-test 13 | args: 14 | - --django 15 | - id: pretty-format-json 16 | args: 17 | - --autofix 18 | - --indent=2 19 | - id: requirements-txt-fixer 20 | - id: trailing-whitespace 21 | - repo: git://github.com/guykisel/prospector-mirror 22 | sha: 00fbd80101566b1b9c873c71f2ab7b95b8bd0a7d 23 | hooks: 24 | - id: prospector 25 | -------------------------------------------------------------------------------- /.prospector.yaml: -------------------------------------------------------------------------------- 1 | output-format: grouped 2 | 3 | strictness: high 4 | test-warnings: false 5 | doc-warnings: false 6 | autodetect: true 7 | member-warnings: false 8 | 9 | ignore-paths: 10 | - docs 11 | - demo 12 | - build 13 | - tests 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - "3.5" 5 | - "3.6" 6 | install: 7 | - pip install tox 8 | - pip install tox-travis 9 | - pip install python-coveralls 10 | script: tox 11 | after_success: 12 | - coveralls 13 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | v2.3.0 - 2017/02/22 4 | * Specify settings through environment variable. 5 | * Change settings to lowercase. 6 | 7 | v2.2.1 - 2017/02/21 8 | * Remove Django from requirements. 9 | 10 | v2.2.0 - 2017/02/21 11 | * Add a standalone mode. 12 | * Command can be called as a Django command through manage.py or as a standlone script. 13 | 14 | v2.1.0 - 2016/09/29 15 | * Create helpers to handle resources and providers and API refactor to use these helpers. 16 | * Create django management command that provides current status of a resource. 17 | 18 | v2.0.0 - 2016/09/2 19 | * Raise exception if incorrect provider given in settings. 20 | * Group providers by resource, adding two by default. *Health* resource manages health checks and *Stats* resource 21 | reports a detailed status. 22 | * Adds HTML views to display status of resources and providers. 23 | 24 | v1.3.0 - 2016/08/31 25 | * Separate api from main application. 26 | 27 | v1.2.1 - 2016/08/31 28 | * Added compatibility with Django 1.10 29 | 30 | v1.2.0 - 2016/05/17 31 | * Add root view for status api. 32 | * Use BASE_DIR instead of PROJECT_PATH to discover source code repo.+ 33 | * Change default checks to be able to override. 34 | 35 | v1.1.2 - 2016/04/8 36 | * Add source code stats. 37 | 38 | v1.0.5 - 2016/01/20 39 | * Fix setup.py problems with requirements file. 40 | 41 | v1.0.4 - 2015/12/14 42 | * Fix setup.py problems with utf-8 files. 43 | 44 | v1.0.3 - 2015/10/14 45 | * Fix checkers for celery and celery stats. Now displays if a worker isn't running. 46 | 47 | v1.0.2 - 2015/10/14 48 | * Update README and meta info. 49 | 50 | v1.0.0 - 2015/10/14 51 | * Database check. 52 | * Database stats. 53 | * Cache check. 54 | * Celery check. 55 | * Celery stats. 56 | * First stable version. 57 | 58 | v0.1.0 - 2015/10/8 59 | * Initial release. 60 | -------------------------------------------------------------------------------- /CONTRIBUTORS.rst: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | * Miguel Barrientos (`@mbarrientos `_) 4 | -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | del = require('del'), 3 | sass = require('gulp-sass'), 4 | cssmin = require('gulp-minify-css'), 5 | browserify = require('browserify'), 6 | babelify = require('babelify'), 7 | uglify = require('gulp-uglify'), 8 | concat = require('gulp-concat'), 9 | rename = require('gulp-rename'), 10 | flatmap = require('gulp-flatmap'), 11 | eslint = require('gulp-eslint'), 12 | path = require('path'), 13 | source = require('vinyl-source-stream'), 14 | buffer = require('vinyl-buffer'), 15 | pkg = require('./package.json'); 16 | 17 | var gulpArgs = { 18 | cwd: path.join(process.cwd(), pkg.paths.static) 19 | }; 20 | 21 | /** 22 | * Cleaning dist/ folder 23 | */ 24 | gulp.task('clean', function () { 25 | del.sync([pkg.dest.dist + '/**'], gulpArgs); 26 | }); 27 | 28 | /** 29 | * Fonts 30 | */ 31 | gulp.task('fonts', function () { 32 | return gulp.src(pkg.paths.fonts) 33 | .pipe(gulp.dest(pkg.dest.fonts, gulpArgs)) 34 | }); 35 | 36 | /** 37 | * CSS 38 | */ 39 | gulp.task('css', function () { 40 | return gulp.src(pkg.paths.css) 41 | .pipe(cssmin()) 42 | .pipe(gulp.dest(pkg.dest.css, gulpArgs)) 43 | }); 44 | 45 | /** 46 | * Scss compilation 47 | */ 48 | var sassOptions = { 49 | errLogToConsole: true, 50 | outputStyle: 'expanded', 51 | includePaths: ['./node_modules'] 52 | }; 53 | gulp.task('styles', function () { 54 | return gulp.src(pkg.paths.scss, gulpArgs) 55 | .pipe(sass(sassOptions).on('error', sass.logError)) 56 | .pipe(concat(pkg.dest.style)) 57 | .pipe(gulp.dest(pkg.dest.dist, gulpArgs)); 58 | }); 59 | gulp.task('styles:min', function () { 60 | return gulp.src(pkg.paths.scss, gulpArgs) 61 | .pipe(sass(sassOptions).on('error', sass.logError)) 62 | .pipe(concat(pkg.dest.style)) 63 | .pipe(cssmin()) 64 | .pipe(gulp.dest(pkg.dest.dist, gulpArgs)); 65 | }); 66 | 67 | /** 68 | * JSLint/JSHint validation 69 | */ 70 | var eslintOptions = { 71 | baseConfig: { 72 | parserOptions: { 73 | "ecmaVersion": 6, 74 | "sourceType": "module", 75 | "ecmaFeatures": { 76 | "jsx": true 77 | } 78 | } 79 | } 80 | }; 81 | gulp.task('lint', function () { 82 | return gulp.src([pkg.paths.js, pkg.paths.jsx], gulpArgs) 83 | .pipe(eslint(eslintOptions)) 84 | .pipe(eslint.format()) 85 | .pipe(eslint.failAfterError()); 86 | }); 87 | 88 | /** JavaScript compilation */ 89 | gulp.task('js', function () { 90 | return gulp.src(pkg.paths.apps, gulpArgs) 91 | .pipe(flatmap(function (stream, file) { 92 | return browserify(file.path) 93 | .transform(babelify, {presets: ["es2015", "react"]}) 94 | .bundle() 95 | .pipe(source(file.path)) 96 | .pipe(rename({dirname: pkg.dest.apps, extname: '.js'})) 97 | .pipe(gulp.dest(pkg.dest.dist, gulpArgs)); 98 | })); 99 | }); 100 | gulp.task('js:min', function () { 101 | process.env.NODE_ENV = 'production'; 102 | return gulp.src(pkg.paths.apps, gulpArgs) 103 | .pipe(flatmap(function (stream, file) { 104 | return browserify(file.path) 105 | .transform(babelify, {presets: ["es2015", "react"]}) 106 | .bundle() 107 | .pipe(source(file.path)) 108 | .pipe(rename({dirname: pkg.dest.apps, extname: '.js'})) 109 | .pipe(buffer()) 110 | .pipe(uglify()) 111 | .pipe(gulp.dest(pkg.dest.dist, gulpArgs)); 112 | })); 113 | }); 114 | 115 | /** 116 | * Compiling resources and serving application 117 | */ 118 | gulp.task('default:watch', function () { 119 | gulp.watch( 120 | [pkg.paths.js, pkg.paths.jsx] 121 | .map(function (obj) { return path.join(pkg.paths.static, obj)}), 122 | ['lint', 'js'], 123 | gulpArgs 124 | ); 125 | gulp.watch( 126 | [pkg.paths.scss] 127 | .map(function (obj) { return path.join(pkg.paths.static, obj)}), 128 | ['styles'] 129 | ); 130 | }); 131 | gulp.task('default', ['clean', 'fonts', 'css', 'lint', 'styles', 'js', 'default:watch']); 132 | 133 | gulp.task('dist:watch', function () { 134 | gulp.watch( 135 | [pkg.paths.js, pkg.paths.jsx] 136 | .map(function (obj) { return path.join(pkg.paths.static, obj)}), 137 | ['lint', 'js:min'], 138 | gulpArgs 139 | ); 140 | gulp.watch( 141 | [pkg.paths.scss] 142 | .map(function (obj) { return path.join(pkg.paths.static, obj)}), 143 | ['styles:min'] 144 | ); 145 | }); 146 | gulp.task('dist', ['clean', 'fonts', 'css', 'lint', 'styles:min', 'js:min']); 147 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Django Status is a application for Django projects that provides an API to check the status of some parts and some utilities like ping requests. 2 | Copyright (C) 2015 Jose Antonio Perdiguero Lopez 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include CHANGELOG 3 | include README.rst 4 | include CONTRIBUTORS.rst 5 | include requirements.txt 6 | include runtests.py 7 | prune status/static/scss 8 | prune status/static/js 9 | prune demo 10 | graft status 11 | graft tests 12 | global-exclude __pycache__ 13 | global-exclude *.py[co] 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | THIS PROJECT IS DEPRECATED. Check `health-check `_ as it's the follow up. 2 | 3 | ============= 4 | Django Status 5 | ============= 6 | 7 | :Version: 2.3.0 8 | :Status: Production/Stable 9 | :Author: José Antonio Perdiguero López 10 | 11 | Django Status is an application that provides an API to check the status of some parts and some utilities like ping 12 | requests. This application can works as standalone or included in a Django project. 13 | 14 | Quick start 15 | =========== 16 | 17 | #. Install this package using pip:: 18 | 19 | pip install django-status 20 | 21 | 22 | #. Add *PROJECT_PATH* to your django settings module. 23 | #. Add *status* to your **INSTALLED_APPS** settings like this:: 24 | 25 | INSTALLED_APPS = ( 26 | ... 27 | 'status', 28 | ) 29 | 30 | #. Add **Django-status** urls to your project urls:: 31 | 32 | urlpatterns = [ 33 | ... 34 | url(r'^status/', include('status.urls')), 35 | ] 36 | 37 | Check Providers 38 | =============== 39 | Django Status provides a mechanism to add new custom check functions through **check providers**. Each check provider 40 | will generate a new API method with an URL that uses the name of the provider. These functions must accept \*args and 41 | \*\*kwargs and will return a JSON-serializable object through json.dumps() method, for example a ping function:: 42 | 43 | def ping(*args, **kwargs): 44 | return {'pong': True} 45 | 46 | By default **Django status** provides the follow checks: 47 | 48 | Ping 49 | A ping to application. 50 | URL: /api/health/ping 51 | 52 | Databases 53 | Check if databases are running. 54 | URL: /api/health/databases 55 | 56 | Caches 57 | Check if caches are running. 58 | URL: /api/health/caches 59 | 60 | Celery 61 | Check if celery workers defined in settings are running. 62 | URL: /api/health/celery 63 | 64 | Databases stats 65 | Show stats for all databases. 66 | URL: /api/stats/databases 67 | 68 | Celery stats 69 | Show celery worker stats. 70 | URL: /api/stats/celery 71 | 72 | Code 73 | Source code stats such as current active branch, last commit, if debug is active... 74 | URL: /api/stats/code 75 | 76 | Django Status website 77 | ===================== 78 | A website that shows Django Status data is available in this application. It's possible access to follow URLs to get a 79 | detailed view of your system status. Those three pages will show results of providers configured (as explained in 80 | settings section):: 81 | 82 | http://www.website.com/status/ 83 | http://www.website.com/status/health/ 84 | http://www.website.com/status/stats/ 85 | 86 | Django Status API 87 | ================= 88 | Django Status API can be used as a standalone application including only their urls:: 89 | 90 | urlpatterns = [ 91 | ... 92 | url(r'^status/', include('status.api.urls')), 93 | ] 94 | 95 | This API have a single url for each provider, that are grouped by resources. 96 | Each provider can be queried alone, returning his current status:: 97 | 98 | http://your_domain/status/api/health/ping 99 | 100 | Also there is a resource view that will return the status of all providers:: 101 | 102 | http://your_domain/status/api/health 103 | 104 | For last, there is a root view that will return the status of all providers from all resources:: 105 | 106 | http://your_domain/status/api 107 | 108 | Django management commands 109 | ========================== 110 | Django Status provides a django management command to query current status of a resource. This command can be call as:: 111 | 112 | python manage.py status [options] 113 | 114 | To get current status of health checks, and exit with an error if some check is failing:: 115 | 116 | python manage.py status health -e 117 | 118 | Each resource has its own set of options that can be displayed through command help:: 119 | 120 | python manage.py status -h 121 | 122 | Command 123 | ======= 124 | Previous Django command can be used in standalone mode as:: 125 | 126 | django_status [options] 127 | 128 | Settings 129 | ======== 130 | Django status settings can be added directly to Django settings module or create an specific module for them. If a 131 | custom module (or class) is used, you must specify it through **DJANGO_STATUS_SETTINGS** environment variable. 132 | 133 | status_check_providers 134 | ---------------------- 135 | List of additional check providers. Each provider consists in a tuple of name, function complete path, args and kwargs. 136 | Example:: 137 | 138 | status_providers = { 139 | 'resource': ( 140 | ('test', 'application.module.test_function', [1, 2], {'foo': 'bar'}), 141 | ) 142 | } 143 | 144 | Default:: 145 | 146 | providers = getattr(settings, 'status_providers', { 147 | 'health': ( 148 | ('ping', 'status.providers.health.ping', None, None), 149 | ('databases', 'status.providers.django.health.databases', None, None), 150 | ('caches', 'status.providers.django.health.caches', None, None), 151 | ), 152 | 'stats': ( 153 | ('databases', 'status.providers.django.stats.databases', None, None), 154 | ('code', 'status.providers.stats.code', None, None), 155 | ) 156 | } 157 | 158 | status_celery_workers 159 | --------------------- 160 | List of hostname from celery workers to be checked. If any worker is defined, two additional providers listed previously 161 | will be added to default set. 162 | Default:: 163 | 164 | status_celery_workers = () 165 | 166 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "Jose Antonio Perdiguero Lopez" 4 | ], 5 | "dependencies": { 6 | "flux": "^2.1.1", 7 | "jquery": "^3.1.0", 8 | "object-assign": "^4.1.0", 9 | "react": "^15.3.1" 10 | }, 11 | "description": "Django application that adds health checks", 12 | "homepage": "https://github.com/PeRDy/django-status", 13 | "ignore": [ 14 | "**/.*", 15 | "node_modules", 16 | "bower_components", 17 | "status/static/vendor", 18 | "test", 19 | "tests" 20 | ], 21 | "license": "GPLv3", 22 | "main": "", 23 | "name": "django-status", 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /demo/demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perdy/django-status/99afa448c1240a8d877ae362beef31077838fa33/demo/demo/__init__.py -------------------------------------------------------------------------------- /demo/demo/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | 6 | 7 | PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 8 | APP_PATH = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | DEBUG = True 11 | TEMPLATE_DEBUG = DEBUG 12 | 13 | ADMINS = ( 14 | ) 15 | 16 | MANAGERS = ADMINS 17 | 18 | DATABASES = { 19 | 'default': { 20 | 'ENGINE': 'django.db.backends.sqlite3', 21 | 'NAME': ':memory:', 22 | }, 23 | } 24 | 25 | CACHES = { 26 | 'default': { 27 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 28 | 'LOCATION': 'default-cache', 29 | }, 30 | 'demo': { 31 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 32 | 'LOCATION': 'demo-cache', 33 | } 34 | } 35 | 36 | ALLOWED_HOSTS = ['*'] 37 | 38 | TIME_ZONE = 'Europe/Madrid' 39 | 40 | LANGUAGE_CODE = 'en-us' 41 | 42 | SITE_ID = 1 43 | 44 | USE_I18N = True 45 | 46 | USE_L10N = True 47 | 48 | USE_TZ = True 49 | 50 | MEDIA_ROOT = PROJECT_PATH + '/media/' 51 | 52 | MEDIA_URL = '/media/' 53 | 54 | STATIC_ROOT = PROJECT_PATH + '/static/' 55 | 56 | STATIC_URL = '/static/' 57 | 58 | STATICFILES_DIRS = ( 59 | os.path.join(APP_PATH, 'static'), 60 | ) 61 | 62 | STATICFILES_FINDERS = ( 63 | 'django.contrib.staticfiles.finders.FileSystemFinder', 64 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 65 | ) 66 | 67 | SECRET_KEY = '3+&*6eq%(gs1heqnu!fpb%_^3x#s_nh04^7dxa8tgb%meu' 68 | 69 | TEMPLATES = [ 70 | { 71 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 72 | 'DIRS': [], 73 | 'APP_DIRS': True, 74 | 'OPTIONS': { 75 | 'context_processors': [ 76 | 'django.template.context_processors.debug', 77 | 'django.template.context_processors.request', 78 | 'django.contrib.auth.context_processors.auth', 79 | 'django.contrib.messages.context_processors.messages', 80 | ], 81 | }, 82 | }, 83 | ] 84 | 85 | MIDDLEWARE_CLASSES = ( 86 | 'django.middleware.common.CommonMiddleware', 87 | 'django.contrib.sessions.middleware.SessionMiddleware', 88 | 'django.middleware.csrf.CsrfViewMiddleware', 89 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 90 | 'django.contrib.messages.middleware.MessageMiddleware', 91 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 92 | ) 93 | 94 | ROOT_URLCONF = 'demo.urls' 95 | 96 | WSGI_APPLICATION = 'demo.wsgi.application' 97 | 98 | TEMPLATE_DIRS = ( 99 | os.path.join(APP_PATH, 'templates'), 100 | ) 101 | 102 | INSTALLED_APPS = ( 103 | 'django.contrib.auth', 104 | 'django.contrib.contenttypes', 105 | 'django.contrib.sessions', 106 | 'django.contrib.sites', 107 | 'django.contrib.messages', 108 | 'django.contrib.staticfiles', 109 | 'django.contrib.admin', 110 | 'demo', 111 | 'status', 112 | ) 113 | 114 | STATUS_CELERY_WORKERS = ( 115 | 'demo_worker', 116 | ) 117 | -------------------------------------------------------------------------------- /demo/demo/templates/400.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 400 9 | 10 | -------------------------------------------------------------------------------- /demo/demo/templates/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 403 9 | 10 | -------------------------------------------------------------------------------- /demo/demo/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 404 9 | 10 | -------------------------------------------------------------------------------- /demo/demo/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 500 9 | 10 | -------------------------------------------------------------------------------- /demo/demo/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block title %}Django Status{% endblock title %} 11 | 12 | {% block css %} 13 | {% endblock css %} 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.conf import settings 4 | from django.conf.urls import include, url 5 | from django.conf.urls.static import static 6 | from django.contrib import admin 7 | 8 | admin.autodiscover() 9 | 10 | urlpatterns = [ 11 | url(r'^admin/', include(admin.site.urls)), 12 | 13 | # Status urls 14 | url(r'^status/', include('status.urls')) 15 | ] 16 | 17 | if settings.DEBUG: 18 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 19 | -------------------------------------------------------------------------------- /demo/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | WSGI config for demo project. 4 | 5 | This module contains the WSGI application used by Django's development server 6 | and any production WSGI deployments. It should expose a module-level variable 7 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 8 | this application via the ``WSGI_APPLICATION`` setting. 9 | 10 | Usually you will have the standard Django WSGI application here, but it also 11 | might make sense to replace the whole Django WSGI application with a custom one 12 | that later delegates to the Django one. For example, you could introduce WSGI 13 | middleware here, or combine a Django application with an application of another 14 | framework. 15 | 16 | """ 17 | import os 18 | 19 | from django.core.wsgi import get_wsgi_application 20 | 21 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'demo.settings') 22 | 23 | # This application object is used by any WSGI server configured to use this 24 | # file. This includes Django's development server, if the WSGI_APPLICATION 25 | # setting points here. 26 | application = get_wsgi_application() 27 | 28 | # Apply WSGI middleware here. 29 | # from helloworld.wsgi import HelloWorldApplication 30 | # application = HelloWorldApplication(application) 31 | -------------------------------------------------------------------------------- /demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Jose Antonio Perdiguero Lopez", 3 | "browser": { 4 | "bootstrap": "./node_modules/bootstrap/dist/js/bootstrap.min.js" 5 | }, 6 | "browserify": { 7 | "transform": [ 8 | "browserify-shim" 9 | ] 10 | }, 11 | "browserify-shim": { 12 | "bootstrap": { 13 | "depends": [ 14 | "jquery:jQuery", 15 | "tether:Tether" 16 | ] 17 | } 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/PeRDy/django-status/issues" 21 | }, 22 | "dependencies": { 23 | "bootstrap": "^4.0.0-alpha", 24 | "flux": "^2.1.1", 25 | "font-awesome": "^4.6.3", 26 | "highlight.js": "^9.7.0", 27 | "jquery": "^3.1.0", 28 | "keymirror": "^0.1.1", 29 | "object-assign": "^4.1.0", 30 | "prismjs": "^1.5.1", 31 | "react": "^15.3.1", 32 | "react-dom": "^15.3.1" 33 | }, 34 | "description": "Application that provides an API to check the status of some parts and some utilities like ping", 35 | "dest": { 36 | "apps": "./apps", 37 | "css": "./status/css", 38 | "dist": "./status", 39 | "fonts": "./status/fonts", 40 | "style": "./css/style.css" 41 | }, 42 | "devDependencies": { 43 | "babel": "^6.5.2", 44 | "babel-preset-es2015": "^6.14.0", 45 | "babel-preset-react": "^6.11.1", 46 | "babelify": "^7.3.0", 47 | "browserify": "^13.1.0", 48 | "browserify-shim": "^3.8.12", 49 | "del": "^2.2.2", 50 | "eslint": "^3.4.0", 51 | "gulp": "^3.9.1", 52 | "gulp-concat": "^2.6.0", 53 | "gulp-eslint": "^3.0.1", 54 | "gulp-flatmap": "^1.0.0", 55 | "gulp-minify-css": "^1.2.4", 56 | "gulp-rename": "^1.2.2", 57 | "gulp-sass": "^2.3.2", 58 | "gulp-uglify": "^2.0.0", 59 | "tether": "^1.3.7", 60 | "vinyl-buffer": "^1.0.0", 61 | "vinyl-source-stream": "^1.1.0" 62 | }, 63 | "homepage": "https://github.com/PeRDy/django-status#readme", 64 | "keywords": [ 65 | "django", 66 | "status", 67 | "healthcheck", 68 | "api" 69 | ], 70 | "license": "GPL-3.0", 71 | "main": "js/app.js", 72 | "name": "django-status", 73 | "paths": { 74 | "apps": [ 75 | "./js/*.jsx" 76 | ], 77 | "css": [], 78 | "fonts": [ 79 | "./node_modules/font-awesome/fonts/*" 80 | ], 81 | "html": "*.html", 82 | "js": "./js/**/*.js", 83 | "jsx": "./js/**/*.jsx", 84 | "scss": "./scss/**/*.scss", 85 | "static": "./status/static" 86 | }, 87 | "repository": { 88 | "type": "git", 89 | "url": "git+https://github.com/PeRDy/django-status.git" 90 | }, 91 | "version": "1.0.0" 92 | } 93 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | GitPython>=1.0 2 | pyyaml>=3.12 3 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/env python 2 | # -*- coding: utf-8 -*- 3 | import argparse 4 | import shlex 5 | import sys 6 | 7 | import nose 8 | import prospector.run as prospector 9 | 10 | 11 | class RunTests: 12 | description = 'Run lint and tests' 13 | 14 | def __init__(self): 15 | parsed_args = self.parse_arguments() 16 | self.test_module = parsed_args['test_module'] 17 | self.test_args = shlex.split(' '.join(parsed_args['test_args'])) 18 | self.skip_lint = parsed_args['skip_lint'] 19 | self.skip_tests = parsed_args['skip_tests'] 20 | self.exit_on_fail = parsed_args['exit_on_fail'] 21 | 22 | def add_arguments(self, parser): 23 | parser.add_argument('--skip-lint', action='store_true', help='Skip lint') 24 | parser.add_argument('--skip-tests', action='store_true', help='Skip tests') 25 | parser.add_argument('-e', '--exit-on-fail', action='store_true', help='Exit on step fail') 26 | parser.add_argument('test_module', nargs='*', default=['.'], help='Module to test') 27 | parser.add_argument('--test_args', action='store', default=[], nargs=argparse.REMAINDER, 28 | help='Extra arguments to pass to tests') 29 | 30 | def parse_arguments(self): 31 | parser = argparse.ArgumentParser(description=self.description) 32 | self.add_arguments(parser) 33 | return {k: v for k, v in vars(parser.parse_args()).items()} 34 | 35 | def tests(self): 36 | argv = ['nosetests'] + self.test_module + self.test_args 37 | result = nose.run(argv=argv) 38 | return (result + 1) % 2 # Change 0 to 1 and 1 to 0 39 | 40 | def lint(self): 41 | argv = sys.argv 42 | sys.argv = [] 43 | result = prospector.main() 44 | sys.argv = argv 45 | return result 46 | 47 | def _check_exit(self, result): 48 | print('Return code: {:d}'.format(result)) 49 | if self.exit_on_fail and result: 50 | sys.exit(result) 51 | 52 | def run(self): 53 | if not self.skip_lint: 54 | self._check_exit(self.lint()) 55 | 56 | if not self.skip_tests: 57 | self._check_exit(self.tests()) 58 | 59 | 60 | if __name__ == '__main__': 61 | sys.exit(RunTests().run()) 62 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.3.0 3 | commit = True 4 | tag = True 5 | 6 | [metadata] 7 | description-file = README.rst 8 | 9 | [wheel] 10 | universal = 1 11 | 12 | [bumpversion:file:status/__init__.py] 13 | search = __version__ = '{current_version}' 14 | replace = __version__ = '{new_version}' 15 | 16 | [bumpversion:file:setup.py] 17 | search = version={current_version} 18 | replace = {new_version} 19 | 20 | [bumpversion:file:README.rst] 21 | search = :Version: {current_version} 22 | replace = :Version: {new_version} 23 | 24 | [nosetests] 25 | verbosity = 1 26 | detailed-errors = 1 27 | with-coverage = 1 28 | cover-package = task_dispatcher 29 | cover-inclusive = 1 30 | cover-min-percentage = 90 31 | 32 | [coverage:run] 33 | source = . 34 | branch = True 35 | omit = 36 | *settings* 37 | *__init__.py 38 | *urls* 39 | .tox* 40 | *tests* 41 | */migrations/* 42 | */features/* 43 | *manage.py 44 | *wsgi.py 45 | *celery.py 46 | */apps.py 47 | run* 48 | 49 | [coverage:report] 50 | show_missing = True 51 | ignore_errors = True 52 | fail_under = 90 53 | exclude_lines = 54 | pragma: no cover 55 | 56 | raise AssertionError 57 | raise NotImplementedError 58 | 59 | if 0: 60 | if __name__ == .__main__.: 61 | 62 | def __repr__ 63 | if self\.debug 64 | if settings\.DEBUG 65 | 66 | [coverage:paths] 67 | source = ./ 68 | 69 | [coverage:html] 70 | directory = .ci_report/coverage_html/ 71 | 72 | [coverage:xml] 73 | output = .ci_report/coverage.xml 74 | 75 | [pep8] 76 | max-line-length = 120 77 | 78 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import shutil 5 | import subprocess 6 | import sys 7 | 8 | from pip.download import PipSession 9 | from pip.req import parse_requirements as requirements 10 | from setuptools import setup, Command 11 | 12 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 13 | 14 | if sys.version_info[0] == 2: 15 | from codecs import open 16 | 17 | 18 | def parse_requirements(req_file): 19 | return [str(req.req) for req in requirements(req_file, session=PipSession())] 20 | 21 | 22 | class Gulp(Command): 23 | description = 'Run gulp' 24 | user_options = [ 25 | ('task=', 't', 'gulp task') 26 | ] 27 | 28 | def initialize_options(self): 29 | self.task = 'dist' 30 | 31 | def finalize_options(self): 32 | pass 33 | 34 | def run(self): 35 | subprocess.call(['gulp', self.task]) 36 | 37 | 38 | class Dist(Command): 39 | description = 'Generate static files and create dist package' 40 | user_options = [ 41 | ('clean', 'c', 'clean dist directories before build (default: false)') 42 | ] 43 | boolean_options = ['clean'] 44 | 45 | def initialize_options(self): 46 | self.clean = False 47 | 48 | def finalize_options(self): 49 | pass 50 | 51 | def run(self): 52 | if self.clean: 53 | shutil.rmtree('build', ignore_errors=True) 54 | shutil.rmtree('dist', ignore_errors=True) 55 | shutil.rmtree('django_status.egg-info', ignore_errors=True) 56 | 57 | self.run_command('gulp') 58 | self.run_command('sdist') 59 | self.run_command('bdist_wheel') 60 | 61 | # Read requirements 62 | _requirements_file = os.path.join(BASE_DIR, 'requirements.txt') 63 | _tests_requirements_file = os.path.join(BASE_DIR, 'tests/requirements.txt') 64 | _REQUIRES = parse_requirements(_requirements_file) 65 | _TESTS_REQUIRES = parse_requirements(_tests_requirements_file) 66 | 67 | # Read description 68 | with open(os.path.join(BASE_DIR, 'README.rst'), encoding='utf-8') as f: 69 | _LONG_DESCRIPTION = f.read() 70 | 71 | _CLASSIFIERS = ( 72 | 'Development Status :: 5 - Production/Stable', 73 | 'Framework :: Django', 74 | 'Intended Audience :: Developers', 75 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 76 | 'Natural Language :: English', 77 | 'Programming Language :: Python', 78 | 'Programming Language :: Python :: 2', 79 | 'Programming Language :: Python :: 3', 80 | 'Topic :: Software Development :: Libraries :: Python Modules', 81 | ) 82 | _KEYWORDS = ' '.join([ 83 | 'python', 84 | 'django', 85 | 'database', 86 | 'cache', 87 | 'celery', 88 | 'status', 89 | 'check' 90 | ]) 91 | 92 | setup( 93 | name='django-status', 94 | version='2.3.0', 95 | description='Application that provides an API to check the status of some parts and some utilities like ping.', 96 | long_description=_LONG_DESCRIPTION, 97 | author='José Antonio Perdiguero López', 98 | author_email='perdy.hh@gmail.com', 99 | maintainer='José Antonio Perdiguero López', 100 | maintainer_email='perdy.hh@gmail.com', 101 | url='https://github.com/PeRDy/django-status', 102 | download_url='https://github.com/PeRDy/django-status', 103 | packages=[ 104 | 'status', 105 | ], 106 | include_package_data=True, 107 | install_requires=_REQUIRES, 108 | tests_require=_TESTS_REQUIRES, 109 | extras_require={ 110 | 'dev': [ 111 | 'setuptools', 112 | 'pip', 113 | 'wheel', 114 | 'prospector', 115 | 'twine', 116 | 'bumpversion', 117 | 'pre-commit', 118 | 'nose' 119 | 'tox', 120 | ] 121 | }, 122 | license='GPLv3', 123 | zip_safe=False, 124 | keywords=_KEYWORDS, 125 | classifiers=_CLASSIFIERS, 126 | cmdclass={ 127 | 'gulp': Gulp, 128 | 'dist': Dist, 129 | }, 130 | entry_points={ 131 | 'console_scripts': [ 132 | 'django_status = status.__main__:main', 133 | ], 134 | } 135 | ) 136 | -------------------------------------------------------------------------------- /status/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Django Status application. 4 | """ 5 | __version__ = '2.3.0' 6 | __license__ = 'GPLv3' 7 | 8 | __author__ = 'José Antonio Perdiguero López' 9 | __email__ = 'perdy.hh@gmail.com' 10 | 11 | __url__ = 'https://github.com/PeRDy/django-status' 12 | __description__ = 'Application that provides an API to check the status of some parts and some utilities like ping.' 13 | 14 | default_app_config = 'status.apps.Status' 15 | -------------------------------------------------------------------------------- /status/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | 4 | from status.commands import StatusCommand 5 | 6 | __all__ = ['main'] 7 | 8 | 9 | def main(): 10 | return StatusCommand().run() 11 | 12 | 13 | if __name__ == '__main__': 14 | sys.exit(main()) 15 | -------------------------------------------------------------------------------- /status/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perdy/django-status/99afa448c1240a8d877ae362beef31077838fa33/status/api/__init__.py -------------------------------------------------------------------------------- /status/api/mixins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.core.urlresolvers import reverse 3 | 4 | 5 | class ProviderMixin: 6 | def get_provider_url(self, request, resource, name): 7 | """ 8 | Get provider url given resource and provider name. 9 | 10 | :param request: Django Request 11 | :param resource: 12 | :param name: 13 | :return: 14 | """ 15 | return request.build_absolute_uri(reverse('status:api_{}_{}'.format(resource, name))) 16 | -------------------------------------------------------------------------------- /status/api/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | URLs. 4 | """ 5 | from itertools import chain 6 | 7 | from django.conf.urls import url 8 | 9 | from status.settings import settings 10 | from status.api.views import RootAPIView, ProviderAPIView, ResourceAPIView 11 | 12 | 13 | def get_provider_urls(resource, providers): 14 | # Resource root view 15 | urls = [url(r'^{}/?$'.format(resource), ResourceAPIView.as_view(resource=resource), 16 | name='api_{}_root'.format(resource))] 17 | 18 | # Resource providers views 19 | urls += [url(r'^{}/{}/?$'.format(resource, n), 20 | ProviderAPIView.as_view(provider=p, provider_name=n, provider_args=args, provider_kwargs=kwargs), 21 | name='api_{}_{}'.format(resource, n)) 22 | for n, p, args, kwargs in providers] 23 | 24 | return urls 25 | 26 | 27 | providers_urls = chain.from_iterable([get_provider_urls(r, p) for r, p in settings.providers.items()]) 28 | 29 | urlpatterns = list(providers_urls) 30 | 31 | urlpatterns += [ 32 | url(r'^$', RootAPIView.as_view()) 33 | ] 34 | -------------------------------------------------------------------------------- /status/api/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Views for status API. 4 | """ 5 | from django.http import JsonResponse 6 | from django.views.generic import View 7 | from django.views.generic.base import ContextMixin 8 | 9 | from status.settings import settings 10 | from status.api.mixins import ProviderMixin 11 | from status.providers.base import Provider, Resource 12 | 13 | __all__ = ['ProviderAPIView', 'RootAPIView'] 14 | 15 | 16 | class JSONResponseMixin(object): 17 | """ 18 | A mixin that can be used to render a JSON response. 19 | """ 20 | 21 | def render_to_json_response(self, context, **response_kwargs): 22 | """ 23 | Returns a JSON response, transforming 'context' to make the payload. 24 | """ 25 | return JsonResponse( 26 | self.get_data(context), 27 | **response_kwargs 28 | ) 29 | 30 | def get_data(self, context): 31 | """ 32 | Returns an object that will be serialized as JSON by json.dumps(). 33 | """ 34 | return context or {} 35 | 36 | 37 | class JSONView(JSONResponseMixin, View): 38 | """ 39 | View that returns a JSON response. 40 | """ 41 | 42 | def render_to_response(self, context, **response_kwargs): 43 | return self.render_to_json_response(context, **response_kwargs) 44 | 45 | 46 | class APIView(JSONView, ContextMixin): 47 | """ 48 | Base class for API views. 49 | """ 50 | 51 | def get(self, request, *args, **kwargs): 52 | context = self.get_context_data(**kwargs) 53 | return self.render_to_response(context) 54 | 55 | def post(self, request, *args, **kwargs): 56 | return self.get(request, *args, **kwargs) 57 | 58 | def put(self, request, *args, **kwargs): 59 | return self.get(request, *args, **kwargs) 60 | 61 | def delete(self, request, *args, **kwargs): 62 | return self.get(request, *args, **kwargs) 63 | 64 | 65 | class RootAPIView(ProviderMixin, APIView): 66 | """ 67 | Root API view that routes to each single ProviderAPIView 68 | """ 69 | 70 | def __init__(self): 71 | super(RootAPIView, self).__init__() 72 | self.resources = {resource: Resource(resource) for resource in settings.providers} 73 | 74 | def get(self, request, *args, **kwargs): 75 | """ 76 | Create an object that contains all resources and within them, all providers with their current status. 77 | 78 | Mapping of resource to another mapping of provider to status. 79 | Resource -> {Provider -> Status} 80 | 81 | :param request: Request. 82 | :return: Rendered response. 83 | """ 84 | context = {resource.name: resource() for resource in self.resources.values()} 85 | return self.render_to_response(context) 86 | 87 | def options(self, request, *args, **kwargs): 88 | """ 89 | List all providers of current resource with their absolute urls for each resource 90 | 91 | Mapping of resource to another mapping of provider to url. 92 | Resource -> {Provider -> URL} 93 | 94 | :param request: Request. 95 | :return: Rendered response. 96 | """ 97 | context = {resource.name: {provider_name: self.get_provider_url(request, resource.name, provider_name) 98 | for provider_name in resource.providers.keys()} 99 | for resource in self.resources.values()} 100 | return self.render_to_response(context) 101 | 102 | 103 | class ResourceAPIView(ProviderMixin, APIView): 104 | """ 105 | Root API view for a resource. List all checks 106 | """ 107 | resource = None 108 | 109 | def __init__(self, resource, **kwargs): 110 | super(ResourceAPIView, self).__init__() 111 | self.resource = Resource(resource) 112 | 113 | def get_context_data(self, **kwargs): 114 | return self.resource() 115 | 116 | def options(self, request, *args, **kwargs): 117 | """ 118 | List all providers of current resource with their absolute urls. 119 | 120 | Mapping of provider to url. 121 | Provider -> URL 122 | 123 | :param request: Request. 124 | :return: Rendered response. 125 | """ 126 | context = {name: self.get_provider_url(request, self.resource.name, name) 127 | for name in self.resource.providers.keys()} 128 | return self.render_to_response(context) 129 | 130 | 131 | class ProviderAPIView(APIView): 132 | """ 133 | Specific class that uses a given provider. 134 | """ 135 | provider = None 136 | provider_name = None 137 | provider_args = None 138 | provider_kwargs = None 139 | 140 | def __init__(self, provider_name, provider, provider_args, provider_kwargs, **kwargs): 141 | self.provider = Provider(provider_name, provider, provider_args, provider_kwargs) 142 | super(ProviderAPIView, self).__init__(**kwargs) 143 | 144 | def get_context_data(self): 145 | return self.provider() 146 | -------------------------------------------------------------------------------- /status/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Django application config module. 3 | """ 4 | from django.apps import AppConfig 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | from status.settings import settings 8 | 9 | 10 | class Status(AppConfig): 11 | name = 'status' 12 | verbose_name = _('Status') 13 | 14 | def ready(self): 15 | settings.build_from_django() 16 | -------------------------------------------------------------------------------- /status/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import sys 4 | from argparse import ArgumentParser 5 | from typing import Dict, Any 6 | 7 | import yaml 8 | 9 | from status.providers import Resource 10 | from status.settings import settings 11 | 12 | 13 | class StatusCommand: 14 | """ 15 | Show status to console. 16 | """ 17 | STATUS_HEALTH = 'health' 18 | STATUS_STATS = 'stats' 19 | STATUS_CHOICES = (STATUS_HEALTH, STATUS_STATS) 20 | 21 | OUTPUT_FORMAT_JSON = 'json' 22 | OUTPUT_FORMAT_YAML = 'yaml' 23 | OUTPUT_FORMAT_CHOICES = (OUTPUT_FORMAT_JSON, OUTPUT_FORMAT_YAML) 24 | 25 | description = 'Status command that provides a common entry point for running different checks providers' 26 | 27 | def __init__(self, parse_args=True, stdout=sys.stdout, stderr=sys.stderr): 28 | """ 29 | Command for running producer, consumer and scheduler processes along with some other utilities. 30 | 31 | :param parse_args: If true, parse sys args. 32 | :param stdout: Standard output. 33 | :param stderr: Standard error. 34 | """ 35 | self.args = self.parse_arguments() if parse_args else {} 36 | self.stdout = stdout 37 | self.stderr = stderr 38 | 39 | def add_arguments(self, parser: ArgumentParser, parser_class=None): 40 | subparsers_kwargs = {} 41 | if parser_class: 42 | subparsers_kwargs['parser_class'] = lambda **kwargs: parser_class(self, **kwargs) 43 | else: 44 | parser.add_argument('-s', '--settings', help='Settings module') 45 | 46 | subparsers = parser.add_subparsers(title='check', description='Status checks', 47 | dest='check', **subparsers_kwargs) 48 | subparsers.required = True 49 | 50 | # Health check 51 | parser_health = subparsers.add_parser(self.STATUS_HEALTH, help='Health checks') 52 | parser_health.add_argument( 53 | '-f', '--output_format', type=str, choices=self.OUTPUT_FORMAT_CHOICES, default=self.OUTPUT_FORMAT_YAML, 54 | help='Output format' 55 | ) 56 | parser_health.add_argument( 57 | '-e', '--error_on_fail', action='store_true', default=False, help='Output format' 58 | ) 59 | parser_health.set_defaults(func=self.health) 60 | 61 | # Stats 62 | parser_stats = subparsers.add_parser(self.STATUS_STATS, help='Components stats') 63 | parser_stats.add_argument( 64 | '-f', '--output_format', type=str, choices=self.OUTPUT_FORMAT_CHOICES, default=self.OUTPUT_FORMAT_YAML, 65 | help='Output format' 66 | ) 67 | parser_stats.set_defaults(func=self.stats) 68 | 69 | def parse_arguments(self) -> Dict[str, Any]: 70 | """ 71 | Parse sys args and transform it into a dict. 72 | 73 | :return: Arguments parsed in dict form. 74 | """ 75 | parser = ArgumentParser(description=self.description) 76 | self.add_arguments(parser) 77 | return {k: v for k, v in vars(parser.parse_args()).items()} 78 | 79 | def health(self, output_format, *args, **kwargs): 80 | resource = Resource(kwargs['check']) 81 | 82 | data = resource() 83 | 84 | if output_format == self.OUTPUT_FORMAT_JSON: 85 | output = json.dumps(data) 86 | else: 87 | output = yaml.safe_dump(data, default_flow_style=False) 88 | 89 | self.stdout.write(output) 90 | 91 | if kwargs['error_on_fail'] and not all([value for provider in data.values() for value in provider.values()]): 92 | sys.exit(1) 93 | 94 | def stats(self, output_format, *args, **kwargs): 95 | resource = Resource(kwargs['check']) 96 | 97 | if output_format == self.OUTPUT_FORMAT_JSON: 98 | output = json.dumps(resource()) 99 | else: 100 | output = yaml.safe_dump(resource(), default_flow_style=False) 101 | 102 | self.stdout.write(output) 103 | 104 | def run(self, **kwargs): 105 | """ 106 | Command entrypoint. 107 | """ 108 | kwargs = kwargs or self.args 109 | 110 | if not kwargs: 111 | raise ValueError('Arguments must be passed or parsed previously') 112 | 113 | if kwargs['settings']: 114 | settings.build_from_module(module=kwargs['settings']) 115 | 116 | command = kwargs['func'] 117 | return command(**kwargs) 118 | -------------------------------------------------------------------------------- /status/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perdy/django-status/99afa448c1240a8d877ae362beef31077838fa33/status/management/__init__.py -------------------------------------------------------------------------------- /status/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perdy/django-status/99afa448c1240a8d877ae362beef31077838fa33/status/management/commands/__init__.py -------------------------------------------------------------------------------- /status/management/commands/status.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | try: 3 | from django.core.management.base import BaseCommand, CommandParser 4 | except ImportError: 5 | BaseCommand = object 6 | 7 | from status.commands import StatusCommand 8 | 9 | 10 | class Command(BaseCommand): 11 | """ 12 | Wrapper that makes a Django command from TaskDispatcherCommand. 13 | """ 14 | help = StatusCommand.description 15 | 16 | def __init__(self, *args, **kwargs): 17 | super(Command, self).__init__(*args, **kwargs) 18 | self.command = StatusCommand(parse_args=False, stdout=self.stdout, stderr=self.stderr) 19 | 20 | def add_arguments(self, parser): 21 | self.command.add_arguments(parser=parser, parser_class=CommandParser) 22 | 23 | def handle(self, *args, **options): 24 | self.command.run(**options) 25 | -------------------------------------------------------------------------------- /status/providers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from status.providers.base import * # noqa 3 | -------------------------------------------------------------------------------- /status/providers/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import importlib 3 | 4 | from status.settings import settings 5 | 6 | __all__ = ['Provider', 'Resource'] 7 | 8 | 9 | class Provider: 10 | """ 11 | Wrapper that handles a check provider, giving easy access to them with dynamic imports from string. 12 | """ 13 | 14 | def __init__(self, name, provider, args, kwargs): 15 | """ 16 | Create a provider. 17 | 18 | :param name: Provider name. 19 | :param provider: Provider function or string. 20 | :param args: Provider args. 21 | :param kwargs: Provider kwargs. 22 | """ 23 | self.name = name 24 | 25 | if isinstance(provider, str): 26 | provider_module, provider_func = provider.rsplit('.', 1) 27 | module = importlib.import_module(provider_module) 28 | self.provider = getattr(module, provider_func, None) 29 | if self.provider is None: 30 | raise ValueError('Provider not found: %s' % (provider,)) 31 | else: 32 | self.provider = provider 33 | 34 | self.args = args or () 35 | self.kwargs = kwargs or {} 36 | 37 | def __call__(self, *args, **kwargs): 38 | """ 39 | Return provider results. 40 | 41 | :param args: Provider args. 42 | :param kwargs: Provider kwargs 43 | :return: Results after evaluates provider. 44 | """ 45 | if not args: 46 | args = self.args 47 | 48 | if not kwargs: 49 | kwargs = self.kwargs 50 | 51 | return self.provider(*args, **kwargs) 52 | 53 | 54 | class Resource: 55 | """ 56 | Wrapper that handles a whole resource with its providers. 57 | """ 58 | 59 | def __init__(self, name): 60 | """ 61 | Create a resource and all its providers. 62 | 63 | :param name: Resource name 64 | """ 65 | self.name = name 66 | try: 67 | self.providers = {name: Provider(name=name, provider=provider, args=args, kwargs=kwargs) 68 | for name, provider, args, kwargs in settings.providers[self.name]} 69 | except KeyError: 70 | raise ValueError("Resource doesn't exists: %s" % (self.name,)) 71 | 72 | def __call__(self, *args, **kwargs): 73 | """ 74 | Return results from all providers of this resource. 75 | 76 | :return: Results after evaluate each provider. 77 | """ 78 | return {name: provider() for name, provider in self.providers.items()} 79 | -------------------------------------------------------------------------------- /status/providers/celery/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perdy/django-status/99afa448c1240a8d877ae362beef31077838fa33/status/providers/celery/__init__.py -------------------------------------------------------------------------------- /status/providers/celery/health.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Built-in Celery check providers. 4 | """ 5 | 6 | from status.utils import FakeChecker 7 | 8 | try: 9 | from celery.task import control 10 | 11 | celery_inspect = control.inspect() 12 | except ImportError: 13 | celery_inspect = FakeChecker() 14 | 15 | 16 | def celery(workers, *args, **kwargs): 17 | """ 18 | Check if given celery workers are running. 19 | 20 | :param workers: List of workers to be checked. 21 | :return: Status of each worker. 22 | """ 23 | try: 24 | ping_response = celery_inspect.ping() or {} 25 | active_workers = ping_response.keys() 26 | workers_status = {w: w in active_workers for w in workers} 27 | except (AttributeError, OSError): 28 | workers_status = None 29 | 30 | return workers_status 31 | -------------------------------------------------------------------------------- /status/providers/celery/stats.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Built-in Celery check providers. 4 | """ 5 | 6 | from status.utils import FakeChecker 7 | 8 | try: 9 | from celery.task import control 10 | 11 | celery_inspect = control.inspect() 12 | except ImportError: 13 | celery_inspect = FakeChecker() 14 | 15 | 16 | def celery(workers, *args, **kwargs): 17 | """ 18 | Retrieve the stats data of given celery workers. 19 | 20 | :param workers: List of workers. 21 | :return: Stats data of each worker. 22 | """ 23 | try: 24 | active_workers = celery_inspect.stats() or {} 25 | workers_stats = {k: v for k, v in active_workers.items() if k in workers} 26 | except AttributeError: 27 | workers_stats = None 28 | 29 | return workers_stats 30 | -------------------------------------------------------------------------------- /status/providers/django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perdy/django-status/99afa448c1240a8d877ae362beef31077838fa33/status/providers/django/__init__.py -------------------------------------------------------------------------------- /status/providers/django/health.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Built-in Django check providers. 4 | """ 5 | import datetime 6 | 7 | from django.core.cache import caches as django_caches 8 | from django.db import connections 9 | 10 | from status.settings import settings 11 | 12 | 13 | def databases(*args, **kwargs): 14 | """ 15 | Check database status. 16 | 17 | :return: Status of each database. 18 | """ 19 | status = {} 20 | for connection in connections.all(): 21 | try: 22 | connection.connect() 23 | status[connection.alias] = connection.is_usable() 24 | except: 25 | status[connection.alias] = False 26 | 27 | return status 28 | 29 | 30 | def caches(*args, **kwargs): 31 | """ 32 | Check caches status. 33 | 34 | :return: Status of each cache. 35 | """ 36 | caches_aliases = settings.caches.keys() 37 | value = datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S') 38 | 39 | status = {} 40 | for alias in caches_aliases: 41 | try: 42 | cache = django_caches[alias] 43 | cache.set('django_status_test_cache', value) 44 | status[alias] = True 45 | except: 46 | status[alias] = False 47 | 48 | return status 49 | -------------------------------------------------------------------------------- /status/providers/django/stats.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Built-in Django check providers. 4 | """ 5 | 6 | from django.db import connections 7 | 8 | 9 | def databases(*args, **kwargs): 10 | """ 11 | Retrieve the stats data of each database. 12 | 13 | :return: Stats data of each database. 14 | """ 15 | stats = {} 16 | for connection in connections.all(): 17 | try: 18 | connection.connect() 19 | except: 20 | is_usable = False 21 | else: 22 | is_usable = connection.is_usable() 23 | finally: 24 | stats[connection.alias] = { 25 | 'vendor': connection.vendor, 26 | 'is_usable': is_usable, 27 | 'allow_thread_sharing': connection.allow_thread_sharing, 28 | 'autocommit': connection.autocommit, 29 | 'commit_on_exit': connection.commit_on_exit, 30 | 'in_atomic_block': connection.in_atomic_block, 31 | 'settings': {k: v for k, v in connection.settings_dict.items() if k not in ('USER', 'PASSWORD')} 32 | } 33 | 34 | return stats 35 | -------------------------------------------------------------------------------- /status/providers/health.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Built-in check providers. 4 | """ 5 | 6 | 7 | def ping(*args, **kwargs): 8 | """ 9 | Check if current application is running. 10 | 11 | :return: Pong response. 12 | """ 13 | 14 | return {'pong': True} 15 | -------------------------------------------------------------------------------- /status/providers/stats.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Built-in check providers. 4 | """ 5 | import datetime 6 | 7 | from git import Repo 8 | from git.exc import InvalidGitRepositoryError 9 | 10 | from status.settings import settings 11 | 12 | 13 | def code(*args, **kwargs): 14 | """ 15 | Code stats, such as current branch, debug mode, last commit, etc. 16 | 17 | :return: Source code stats. 18 | """ 19 | stats = {'debug': settings.debug} 20 | try: 21 | scm_stats = {'scm': 'git'} 22 | repo = Repo(settings.base_dir) 23 | 24 | # Branch info 25 | branch = repo.active_branch 26 | scm_stats['branch'] = { 27 | 'local': branch.name, 28 | } 29 | try: 30 | # Try to get remote branch name 31 | scm_stats['branch']['remote'] = branch.tracking_branch().name, 32 | except: 33 | scm_stats['branch']['remote'] = 'unknown' 34 | 35 | # Last commit info 36 | commit = repo.commit(branch) 37 | scm_stats['commit'] = { 38 | 'id': commit.hexsha, 39 | 'summary': commit.summary, 40 | 'author': commit.author.name, 41 | 'date': datetime.datetime.fromtimestamp(commit.authored_date) 42 | } 43 | 44 | # Get tag info 45 | tag = None 46 | tags = repo.tags 47 | tags.reverse() 48 | for t in (t for t in tags if tag is None): 49 | if t.commit == commit: 50 | tag = t 51 | if tag: 52 | scm_stats['tag'] = tag.name 53 | except InvalidGitRepositoryError: 54 | scm_stats = {} 55 | 56 | stats.update(scm_stats) 57 | 58 | return stats 59 | -------------------------------------------------------------------------------- /status/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Settings. 4 | """ 5 | import os 6 | from importlib import import_module 7 | 8 | __all__ = ['settings'] 9 | 10 | 11 | class Settings: 12 | debug = False 13 | base_dir = None 14 | installed_apps = () 15 | caches = {} 16 | celery_workers = () 17 | providers = {'health': (), 'stats': ()} 18 | 19 | def __init__(self): 20 | module_path = os.environ.get('DJANGO_STATUS_SETTINGS') 21 | if module_path: 22 | self.build_from_module(module_path) 23 | 24 | @staticmethod 25 | def import_settings(path): 26 | try: 27 | s = import_module(path) 28 | except ImportError: 29 | m, c = path.rsplit('.', 1) 30 | module = import_module(m) 31 | s = getattr(module, c) 32 | 33 | return s 34 | 35 | def _add_celery_providers(self): 36 | self.providers['health'] += ( 37 | ('celery', 'status.providers.celery.health.celery', None, {'workers': self.celery_workers}), 38 | ) 39 | self.providers['stats'] += ( 40 | ('celery', 'status.providers.celery.stats.celery', None, {'workers': self.celery_workers}), 41 | ) 42 | 43 | def build_from_module(self, module=None): 44 | if isinstance(module, str): 45 | module = self.import_settings(module) 46 | 47 | # Project path defined in settings 48 | self.base_dir = getattr(module, 'base_dir', None) 49 | 50 | # Celery application 51 | self.celery_workers = getattr(module, 'status_celery_workers', ()) 52 | 53 | # Mapping of resources to list of providers 54 | # Each provider is a tuple of application name, provider, args, kwargs 55 | self.providers = getattr(module, 'status_providers', { 56 | 'health': ( 57 | ('ping', 'status.providers.health.ping', None, None), 58 | ), 59 | 'stats': ( 60 | ('code', 'status.providers.stats.code', None, None), 61 | ) 62 | }) 63 | 64 | # If there is celery workers then add celery providers 65 | if self.celery_workers: 66 | self._add_celery_providers() 67 | 68 | def build_from_django(self): 69 | from django.conf import settings as django_settings 70 | 71 | # Django debug settings. 72 | self.debug = getattr(django_settings, 'debug', False) 73 | 74 | # Django installed apps 75 | self.installed_apps = getattr(django_settings, 'installed_apps', []) 76 | self.caches = getattr(django_settings, 'caches', {}) 77 | 78 | # Project path defined in Django settings 79 | self.base_dir = getattr(django_settings, 'base_dir', None) 80 | 81 | # Celery application 82 | self.celery_workers = getattr(django_settings, 'status_celery_workers', ()) 83 | 84 | # Mapping of resources to list of providers 85 | # Each provider is a tuple of application name, provider, args, kwargs 86 | self.providers = getattr(django_settings, 'status_providers', { 87 | 'health': ( 88 | ('ping', 'status.providers.health.ping', None, None), 89 | ('databases', 'status.providers.django.health.databases', None, None), 90 | ('caches', 'status.providers.django.health.caches', None, None), 91 | ), 92 | 'stats': ( 93 | ('databases', 'status.providers.django.stats.databases', None, None), 94 | ('code', 'status.providers.stats.code', None, None), 95 | ) 96 | }) 97 | 98 | # If there is celery workers then add celery providers 99 | if self.celery_workers: 100 | self._add_celery_providers() 101 | 102 | settings = Settings() 103 | -------------------------------------------------------------------------------- /status/static/js/actions/AppActions.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from '../dispatcher/AppDispatcher'; 2 | import AppConstants from '../constants/AppConstants'; 3 | 4 | var AppActions = { 5 | 6 | /** 7 | * Update a single Health check 8 | * @param {string} url The url of Health checks api view 9 | * @param {string} name The name of the Health check item 10 | */ 11 | updateHealth: function(url, name) { 12 | AppDispatcher.handleAction({ 13 | actionType: AppConstants.HEALTH_UPDATE, 14 | url: url, 15 | name: name 16 | }); 17 | }, 18 | 19 | /** 20 | * Update all items from health app. 21 | * @param {string} url The url of Health checks api view 22 | */ 23 | updateHealthAll: function(url) { 24 | AppDispatcher.handleAction({ 25 | actionType: AppConstants.HEALTH_UPDATE_ALL, 26 | url: url 27 | }); 28 | }, 29 | 30 | /** 31 | * Update a single Stats 32 | * @param {string} url The url of Stats api view 33 | * @param {string} name The name of the Stats item 34 | */ 35 | updateStats: function(url, name) { 36 | AppDispatcher.handleAction({ 37 | actionType: AppConstants.STATS_UPDATE, 38 | url: url, 39 | id: name 40 | }); 41 | }, 42 | 43 | /** 44 | * Update all items from stats app. 45 | * @param {string} url The url of Stats api view 46 | */ 47 | updateStatsAll: function(url) { 48 | AppDispatcher.handleAction({ 49 | actionType: AppConstants.STATS_UPDATE_ALL, 50 | url: url 51 | }); 52 | }, 53 | }; 54 | 55 | export default AppActions; 56 | -------------------------------------------------------------------------------- /status/static/js/components/HealthApp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HealthStore from '../stores/HealthStore'; 3 | import HealthItem from './HealthItem.jsx'; 4 | 5 | /** 6 | * Retrieve the current TODO data from the StatusStore 7 | */ 8 | function getState() { 9 | return { 10 | items: HealthStore.getAll() 11 | }; 12 | } 13 | 14 | class HealthApp extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | items: props.items, 19 | name: props.name 20 | }; 21 | this._onChange = () => this.setState(() => { return {items: HealthStore.getAll()} }); 22 | } 23 | 24 | componentDidMount() { 25 | HealthStore.addChangeListener(this._onChange); 26 | } 27 | 28 | componentWillUnmount() { 29 | HealthStore.removeChangeListener(this._onChange); 30 | } 31 | 32 | render() { 33 | var items = []; 34 | 35 | for (var name in this.state.items) { 36 | if (this.state.items.hasOwnProperty(name)) { 37 | items.push( 38 | 39 | ); 40 | } 41 | } 42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | { this.state.name } 49 | 50 | 51 | { items } 52 | 53 | 54 | 55 | 56 | ) 57 | } 58 | } 59 | 60 | HealthApp.propTypes = { 61 | items: React.PropTypes.object.isRequired, 62 | name: React.PropTypes.string 63 | }; 64 | 65 | HealthApp.defaultProps = { 66 | items: {}, 67 | name: "Health Check" 68 | }; 69 | 70 | export default HealthApp; 71 | -------------------------------------------------------------------------------- /status/static/js/components/HealthItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HealthStore from "../stores/HealthStore"; 3 | 4 | class HealthItem extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | name: props.name, 9 | status: props.status 10 | }; 11 | this._onChange = () => this.setState(() => { return {status: HealthStore.get(this.state.name)} }); 12 | } 13 | 14 | healthy() { 15 | var result = true; 16 | var status = this.state.status; 17 | 18 | for (var i in status) { 19 | if (result && status.hasOwnProperty(i) && status[i] === false) { 20 | result = false; 21 | } 22 | } 23 | 24 | return result; 25 | } 26 | 27 | /** 28 | * @return {object} 29 | */ 30 | render() { 31 | var healthy = this.healthy(); 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | { this.state.name } 39 | 40 | ) 41 | } 42 | } 43 | 44 | HealthItem.propTypes = { 45 | name: React.PropTypes.string.isRequired, 46 | status: React.PropTypes.object.isRequired 47 | }; 48 | 49 | HealthItem.defaultProps = { 50 | name: '', 51 | status: {} 52 | }; 53 | 54 | export default HealthItem; 55 | -------------------------------------------------------------------------------- /status/static/js/components/StatsApp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StatsStore from '../stores/StatsStore'; 3 | import StatsItem from './StatsItem.jsx'; 4 | 5 | class StatsApp extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | items: props.items, 10 | name: props.name 11 | }; 12 | this._onChange = () => this.setState(() => { return {items: StatsStore.getAll()} }); 13 | } 14 | 15 | componentDidMount() { 16 | StatsStore.addChangeListener(this._onChange); 17 | } 18 | 19 | componentWillUnmount() { 20 | StatsStore.removeChangeListener(this._onChange); 21 | } 22 | 23 | render() { 24 | var items = []; 25 | 26 | for (var name in this.state.items) { 27 | if (this.state.items.hasOwnProperty(name)) { 28 | items.push( 29 | 30 | ); 31 | } 32 | } 33 | 34 | return ( 35 | 36 | 37 | { items } 38 | 39 | 40 | ) 41 | } 42 | } 43 | 44 | StatsApp.propTypes = { 45 | items: React.PropTypes.object.isRequired, 46 | name: React.PropTypes.string 47 | }; 48 | 49 | StatsApp.defaultProps = { 50 | items: {}, 51 | name: "Stats" 52 | }; 53 | 54 | export default StatsApp; 55 | -------------------------------------------------------------------------------- /status/static/js/components/StatsItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StatsStore from "../stores/StatsStore"; 3 | import StatsItemKey from "./StatsItemKey.jsx"; 4 | 5 | class StatsItem extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | name: props.name, 11 | status: props.status 12 | }; 13 | 14 | this._onChange = () => this.setState(() => { 15 | return {status: StatsStore.get(this.state.name)} 16 | }); 17 | } 18 | 19 | /** 20 | * @return {object} 21 | */ 22 | render() { 23 | var objects = []; 24 | 25 | for (var item in this.state.status) { 26 | if (this.state.status.hasOwnProperty(item)) { 27 | var key = this.state.name + item[0].toUpperCase() + item.slice(1); 28 | objects.push( 29 | 30 | ) 31 | } 32 | } 33 | 34 | return ( 35 | 36 | 37 | { this.state.name } 38 | 39 | 40 | 41 | 42 | { objects } 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | } 50 | 51 | StatsItem.propTypes = { 52 | name: React.PropTypes.string.isRequired, 53 | status: React.PropTypes.object.isRequired 54 | }; 55 | 56 | StatsItem.defaultProps = { 57 | name: '', 58 | status: {} 59 | }; 60 | 61 | export default StatsItem; 62 | -------------------------------------------------------------------------------- /status/static/js/components/StatsItemKey.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StatsStore from "../stores/StatsStore"; 3 | 4 | class StatsItemKey extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | statsChainKeys: props.statsChainKeys, 10 | status: props.status 11 | }; 12 | 13 | this.toggleArrow = this.toggleArrow.bind(this); 14 | 15 | this._onChange = () => this.setState(() => { 16 | var stats = StatsStore.get(this.state.statsChainKeys[0]); 17 | 18 | for (var key in this.state.statsChainKeys.slice(1)) { 19 | stats = stats[key]; 20 | } 21 | 22 | return {status: stats} 23 | }); 24 | } 25 | 26 | getChainID() { 27 | var capitalizeIDs = this.state.statsChainKeys.slice(1).map((value) => { 28 | return value[0].toUpperCase() + value.slice(1); 29 | }); 30 | 31 | return this.state.statsChainKeys[0] + capitalizeIDs.join(''); 32 | } 33 | 34 | name() { 35 | return this.state.statsChainKeys[this.state.statsChainKeys.length - 1]; 36 | } 37 | 38 | toggleArrow() { 39 | this.refs.arrow.classList.toggle('fa-rotate-90'); 40 | } 41 | 42 | render() { 43 | var result = null; 44 | 45 | var id = this.getChainID(); 46 | 47 | if (typeof this.state.status === 'object' && this.state.status instanceof Array) { 48 | // Check if array 49 | items = []; 50 | 51 | for (var item in this.state.status) { 52 | if (this.state.status.hasOwnProperty(item)) { 53 | items.push({ this.state.status[item] }) 54 | } 55 | } 56 | 57 | result = ( 58 | 59 | 60 | 61 | { this.name() } 62 | 63 | 64 | { items } 65 | 66 | 67 | ) 68 | } else if (this.state.status && Object.keys(this.state.status).length > 0 69 | && this.state.status.constructor === Object) { // Isn't empty 70 | // Check if object 71 | var items = []; 72 | var i = 0; 73 | 74 | for (var item in this.state.status) { 75 | if (this.state.status.hasOwnProperty(item)) { 76 | var statsChainKey = this.state.statsChainKeys.slice(); 77 | statsChainKey.push(item); 78 | items.push( 79 | 80 | ) 81 | } 82 | } 83 | 84 | result = ( 85 | 86 | 87 | 88 | { this.name() } 89 | 90 | 91 | { items } 92 | 93 | 94 | ) 95 | } else { 96 | // Simple element 97 | item = this.state.status; 98 | if (typeof item !== "undefined" && item && item.constructor !== Object) { 99 | var value = item.toString(); 100 | } else { 101 | value = "" 102 | } 103 | 104 | result = ( 105 | 106 | { this.name() } 107 | { value } 108 | 109 | ) 110 | } 111 | 112 | return result; 113 | } 114 | } 115 | 116 | StatsItemKey.propTypes = { 117 | statsChainKeys: React.PropTypes.arrayOf(React.PropTypes.string).isRequired, 118 | status: React.PropTypes.any 119 | }; 120 | 121 | StatsItemKey.defaultProps = { 122 | statsChainKeys: [], 123 | status: {} 124 | }; 125 | 126 | export default StatsItemKey; 127 | -------------------------------------------------------------------------------- /status/static/js/components/StatusApp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HealthApp from './HealthApp.jsx'; 3 | import StatsApp from './StatsApp.jsx'; 4 | 5 | /** 6 | * Retrieve the current TODO data from the StatusStore 7 | */ 8 | function getStatusState() { 9 | return {}; 10 | } 11 | 12 | class StatusApp extends React.Component { 13 | render() { 14 | return ( 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | } 22 | 23 | StatusApp.propTypes = {}; 24 | 25 | StatusApp.defaultProps = {}; 26 | 27 | export default StatusApp; 28 | -------------------------------------------------------------------------------- /status/static/js/components/StatusItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class HealthItem extends React.Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | name: props.name, 9 | status: props.status 10 | }; 11 | } 12 | 13 | healthy() { 14 | var result = true; 15 | var check = this.props.status; 16 | 17 | for (var i in check) { 18 | if (result && check.hasOwnProperty(i) && check[i] === false) { 19 | result = false; 20 | } 21 | } 22 | 23 | return result; 24 | } 25 | 26 | /** 27 | * @return {object} 28 | */ 29 | render() { 30 | var name = this.props.name; 31 | var rendered; 32 | if (this.healthy()) { 33 | rendered = ( 34 | 35 | 36 | 37 | 38 | { name } 39 | 40 | ) 41 | } else { 42 | rendered = ( 43 | 44 | 45 | 46 | 47 | { name } 48 | 49 | ) 50 | } 51 | return rendered; 52 | } 53 | } 54 | 55 | HealthItem.propTypes = { 56 | name: React.PropTypes.string.isRequired, 57 | status: React.PropTypes.object.isRequired 58 | }; 59 | 60 | HealthItem.defaultProps = { 61 | name: '', 62 | status: {} 63 | }; 64 | 65 | export default HealthItem; 66 | -------------------------------------------------------------------------------- /status/static/js/constants/AppConstants.js: -------------------------------------------------------------------------------- 1 | import keyMirror from "keymirror"; 2 | 3 | export default keyMirror({ 4 | OK: null, 5 | FAIL: null, 6 | STATUS_UPDATE_ALL: null, 7 | HEALTH_UPDATE: null, 8 | HEALTH_UPDATE_ALL: null, 9 | STATS_UPDATE: null, 10 | STATS_UPDATE_ALL: null 11 | }); 12 | -------------------------------------------------------------------------------- /status/static/js/dispatcher/AppDispatcher.js: -------------------------------------------------------------------------------- 1 | var Dispatcher = require('flux').Dispatcher; 2 | 3 | // Create dispatcher instance 4 | var AppDispatcher = new Dispatcher(); 5 | 6 | // Convenience method to handle dispatch requests 7 | AppDispatcher.handleAction = function(action) { 8 | this.dispatch({ 9 | source: 'VIEW_ACTION', 10 | action: action 11 | }); 12 | }; 13 | 14 | export default AppDispatcher; 15 | -------------------------------------------------------------------------------- /status/static/js/health.jsx: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import 'bootstrap'; 3 | 4 | import React from "react"; 5 | import ReactDOM from "react-dom"; 6 | import HealthApp from "./components/HealthApp.jsx"; 7 | import AppActions from "./actions/AppActions"; 8 | 9 | ReactDOM.render( 10 | , 11 | document.getElementById('app') 12 | ); 13 | 14 | $(document).ready(() => { 15 | var api_url = $('#api_url').html(); 16 | AppActions.updateHealthAll(api_url); 17 | setInterval(() => { AppActions.updateHealthAll(api_url) }, 60000); 18 | }); 19 | -------------------------------------------------------------------------------- /status/static/js/main.jsx: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import 'bootstrap'; 3 | 4 | import React from "react"; 5 | import ReactDOM from "react-dom"; 6 | import StatusApp from "./components/StatusApp.jsx"; 7 | import AppActions from "./actions/AppActions"; 8 | 9 | ReactDOM.render( 10 | , 11 | document.getElementById('app') 12 | ); 13 | 14 | const update = (urls) => { 15 | AppActions.updateHealthAll(urls['health']); 16 | AppActions.updateStatsAll(urls['stats']); 17 | } 18 | 19 | $(document).ready(() => { 20 | var api_urls = { 21 | health: $('#health_url').html(), 22 | stats: $('#stats_url').html() 23 | } 24 | 25 | update(api_urls); 26 | setInterval(() => { update(api_urls) }, 60000); 27 | }); 28 | -------------------------------------------------------------------------------- /status/static/js/stats.jsx: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import 'bootstrap'; 3 | 4 | import React from "react"; 5 | import ReactDOM from "react-dom"; 6 | import StatsApp from "./components/StatsApp.jsx"; 7 | import AppActions from "./actions/AppActions"; 8 | 9 | ReactDOM.render( 10 | , 11 | document.getElementById('app') 12 | ); 13 | 14 | $(document).ready(() => { 15 | var api_url = $('#api_url').html(); 16 | AppActions.updateStatsAll(api_url); 17 | setInterval(() => { AppActions.updateStatsAll(api_url) }, 60000); 18 | }); 19 | -------------------------------------------------------------------------------- /status/static/js/stores/HealthStore.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import events from "events"; 3 | import assign from "object-assign"; 4 | import AppDispatcher from "../dispatcher/AppDispatcher"; 5 | import AppConstants from "../constants/AppConstants"; 6 | 7 | var EventEmitter = events.EventEmitter; 8 | 9 | var CHANGE_EVENT = 'change'; 10 | 11 | var _health = {}; 12 | 13 | /** 14 | * Create an item. 15 | * @param {string} name The name of the Health check 16 | * @param {string} content The content of the Health check 17 | */ 18 | function create(name, content) { 19 | _health[name] = { 20 | name: name, 21 | status: content 22 | }; 23 | } 24 | 25 | /** 26 | * Update an item. 27 | * @param {string} name The name of the Health check 28 | * @param {object} updates An object literal containing only the data to be 29 | * updated. 30 | */ 31 | function update(name, updates) { 32 | _health[name] = assign({}, _health[name], updates); 33 | } 34 | 35 | /** 36 | * Delete an item. 37 | * @param {string} name The name of the Health check 38 | */ 39 | function destroy(name) { 40 | delete _health[name]; 41 | } 42 | 43 | /** 44 | * Update a single Health check. 45 | * @param url Url to query for data 46 | * @param name Health check name 47 | */ 48 | function updateSingle(url, name) { 49 | $.get(url) 50 | .done((data) => { 51 | if (data.hasOwnProperty(name)) { 52 | update(name, data[name]) 53 | } 54 | HealthStore.emitChange(); 55 | }); 56 | } 57 | 58 | /** 59 | * Update all Health checks. 60 | * @param url Url to query for data 61 | */ 62 | function updateAll(url) { 63 | $.get(url) 64 | .done((data) => { 65 | for (var item in data) { 66 | if (data.hasOwnProperty(item)) { 67 | update(item, data[item]) 68 | } 69 | } 70 | HealthStore.emitChange(); 71 | }); 72 | } 73 | 74 | var HealthStore = assign({}, EventEmitter.prototype, { 75 | /** 76 | * Get a single element. 77 | * @param name 78 | * @returns {object} 79 | */ 80 | get: function (name) { 81 | return _health[name]; 82 | }, 83 | 84 | /** 85 | * Get the entire collection. 86 | * @return {object} 87 | */ 88 | getAll: function () { 89 | return _health; 90 | }, 91 | 92 | emitChange: function () { 93 | this.emit(CHANGE_EVENT); 94 | }, 95 | 96 | /** 97 | * @param {function} callback 98 | */ 99 | addChangeListener: function (callback) { 100 | this.on(CHANGE_EVENT, callback); 101 | }, 102 | 103 | /** 104 | * @param {function} callback 105 | */ 106 | removeChangeListener: function (callback) { 107 | this.removeListener(CHANGE_EVENT, callback); 108 | } 109 | }); 110 | 111 | // Register callback to handle all updates 112 | AppDispatcher.register(function (payload) { 113 | var action = payload.action; 114 | 115 | switch (action.actionType) { 116 | case AppConstants.HEALTH_UPDATE_ALL: 117 | updateAll(action.url); 118 | break; 119 | 120 | case AppConstants.HEALTH_UPDATE: 121 | updateSingle(action.url, action.name); 122 | break; 123 | 124 | default: 125 | // no op 126 | } 127 | }); 128 | 129 | export default HealthStore; 130 | -------------------------------------------------------------------------------- /status/static/js/stores/StatsStore.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import events from "events"; 3 | import assign from "object-assign"; 4 | import AppDispatcher from "../dispatcher/AppDispatcher"; 5 | import AppConstants from "../constants/AppConstants"; 6 | 7 | var EventEmitter = events.EventEmitter; 8 | 9 | var CHANGE_EVENT = 'change'; 10 | 11 | var _stats = {}; 12 | 13 | 14 | /** 15 | * Create an item. 16 | * @param {string} name The name of the Stats 17 | * @param {string} content The content of the Stats 18 | */ 19 | function create(name, content) { 20 | _stats[name] = { 21 | name: name, 22 | status: content 23 | }; 24 | } 25 | 26 | /** 27 | * Update an item. 28 | * @param {string} name The name of the Stats 29 | * @param {object} updates An object literal containing only the data to be 30 | * updated. 31 | */ 32 | function update(name, updates) { 33 | _stats[name] = assign({}, _stats[name], updates); 34 | } 35 | 36 | /** 37 | * Delete an item. 38 | * @param {string} name The name of the Stats 39 | */ 40 | function destroy(name) { 41 | delete _stats[name]; 42 | } 43 | /** 44 | * Update a single item. 45 | * @param {string} url Url to query for data 46 | * @param {string} name Health check name 47 | */ 48 | function updateSingle(url, name) { 49 | $.get(url) 50 | .done((data) => { 51 | if (data.hasOwnProperty(name)) { 52 | update(name, data[name]) 53 | } 54 | StatsStore.emitChange(); 55 | }); 56 | } 57 | 58 | /** 59 | * Update all items. 60 | * @param {string} url Url to query for data 61 | */ 62 | function updateAll(url) { 63 | $.get(url) 64 | .done((data) => { 65 | for (var item in data) { 66 | if (data.hasOwnProperty(item)) { 67 | update(item, data[item]) 68 | } 69 | } 70 | StatsStore.emitChange(); 71 | }); 72 | } 73 | 74 | var StatsStore = assign({}, EventEmitter.prototype, { 75 | /** 76 | * Get the entire collection. 77 | * @return {object} 78 | */ 79 | getAll: function () { 80 | return _stats; 81 | }, 82 | 83 | emitChange: function () { 84 | this.emit(CHANGE_EVENT); 85 | }, 86 | 87 | /** 88 | * @param {function} callback 89 | */ 90 | addChangeListener: function (callback) { 91 | this.on(CHANGE_EVENT, callback); 92 | }, 93 | 94 | /** 95 | * @param {function} callback 96 | */ 97 | removeChangeListener: function (callback) { 98 | this.removeListener(CHANGE_EVENT, callback); 99 | } 100 | }); 101 | 102 | // Register callback to handle all updates 103 | AppDispatcher.register(function (payload) { 104 | var action = payload.action; 105 | 106 | switch (action.actionType) { 107 | case AppConstants.STATS_UPDATE_ALL: 108 | updateAll(action.url); 109 | break; 110 | 111 | case AppConstants.STATS_UPDATE: 112 | updateSingle(action.url, action.name); 113 | break; 114 | 115 | default: 116 | // no op 117 | } 118 | }); 119 | 120 | export default StatsStore; 121 | -------------------------------------------------------------------------------- /status/static/js/stores/StatusStore.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import events from "events"; 3 | import assign from "object-assign"; 4 | import AppDispatcher from "../dispatcher/AppDispatcher"; 5 | import AppConstants from "../constants/AppConstants"; 6 | 7 | var EventEmitter = events.EventEmitter; 8 | 9 | var CHANGE_EVENT = 'change'; 10 | 11 | var _status = { 12 | health: {}, 13 | stats: {} 14 | }; 15 | 16 | /** 17 | * Create a Status item. 18 | * @param {string} name The name of the Status 19 | * @param {string} content The content of the Status 20 | */ 21 | function create(type, name, content) { 22 | // Using the current timestamp + random number in place of a real id. 23 | _status[name] = { 24 | name: name, 25 | status: content 26 | }; 27 | } 28 | 29 | /** 30 | * Update a Status item. 31 | * @param {string} name The name of the Status 32 | * @param {object} updates An object literal containing only the data to be 33 | * updated. 34 | */ 35 | function update(name, updates) { 36 | _status[name] = assign({}, _status[name], updates); 37 | } 38 | 39 | /** 40 | * Delete a Status item. 41 | * @param {string} name The name of the Status 42 | */ 43 | function destroy(name) { 44 | delete _status[name]; 45 | } 46 | 47 | var StatusStore = assign({}, EventEmitter.prototype, { 48 | /** 49 | * Get the entire collection of TODOs. 50 | * @return {object} 51 | */ 52 | getAll: function () { 53 | return _status; 54 | }, 55 | 56 | emitChange: function () { 57 | this.emit(CHANGE_EVENT); 58 | }, 59 | 60 | /** 61 | * @param {function} callback 62 | */ 63 | addChangeListener: function (callback) { 64 | this.on(CHANGE_EVENT, callback); 65 | }, 66 | 67 | /** 68 | * @param {function} callback 69 | */ 70 | removeChangeListener: function (callback) { 71 | this.removeListener(CHANGE_EVENT, callback); 72 | } 73 | }); 74 | 75 | // Register callback to handle all updates 76 | AppDispatcher.register(function (action) { 77 | switch (action.actionType) { 78 | case AppConstants.STATUS_UPDATE_ALL: 79 | $.get('/status/api') 80 | .done((data) => { 81 | _status = data; 82 | StatusStore.emitChange(); 83 | }); 84 | break; 85 | 86 | default: 87 | // no op 88 | } 89 | }); 90 | 91 | export default StatusStore; 92 | -------------------------------------------------------------------------------- /status/static/scss/base.scss: -------------------------------------------------------------------------------- 1 | @import "bootstrap/scss/bootstrap"; 2 | @import "font-awesome/scss/font-awesome"; 3 | @import "colors"; 4 | 5 | body { 6 | background-color: $color3; 7 | } 8 | 9 | .card { 10 | background-color: $color1; 11 | color: $color5; 12 | border: 1px solid $color3; 13 | border-radius: 0; 14 | margin-bottom: 0; 15 | 16 | .card-title { 17 | text-transform: capitalize; 18 | } 19 | } 20 | 21 | .list-group-item { 22 | background-color: $color1; 23 | color: $color5; 24 | border-radius: 0; 25 | border: 1px solid $color3; 26 | 27 | &:hover, &:focus { 28 | text-decoration: none; 29 | } 30 | } 31 | 32 | .nested-list-group { 33 | > .list-group { 34 | padding: 0; 35 | overflow: hidden; 36 | 37 | .list-group { 38 | margin-bottom: 0; 39 | padding-left: 1em; 40 | } 41 | 42 | .list-group-item { 43 | border-radius: 0; 44 | 45 | > i { 46 | margin-right: 0.25em; 47 | } 48 | } 49 | } 50 | 51 | .nested-node { 52 | &:not(:last-child) .list-group > .list-group-item:last-child { 53 | margin-bottom: -1px; 54 | } 55 | 56 | &:last-child { 57 | > .list-group-item.collapsed { 58 | margin-bottom: 0; 59 | } 60 | 61 | > .list-group.collapsing { 62 | margin-top: -1px; 63 | } 64 | } 65 | } 66 | 67 | .nested-leaf { 68 | background-color: $color2; 69 | color: $color5; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /status/static/scss/colors.scss: -------------------------------------------------------------------------------- 1 | $color1: rgba(255, 255, 255, 1); 2 | $color2: rgba(239, 239, 239, 1); 3 | $color3: rgba(94, 94, 94, 1); 4 | $color4: rgba(73, 98, 117, 1); 5 | $color5: rgba(25, 133, 161, 1); 6 | -------------------------------------------------------------------------------- /status/templates/status/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | {% block page_title %}{{ _("Status") }}{% endblock %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% block meta %} 24 | {% endblock %} 25 | 26 | {# use the CDN to get bootstrap css. can not be inside compress css block #} 27 | {% block cdn_css %} 28 | {% endblock cdn_css %} 29 | 30 | 31 | {% block css %}{% endblock %} 32 | 33 | 34 | {% block body %} 35 | 37 | 40 | 41 | {% block header %}{% endblock %} 42 | 43 | 44 | 45 | {% block content %}{% endblock %} 46 | 47 | 48 | 52 | 53 | 54 | 55 | 56 | 57 | {% block cdn_js %} 58 | {% endblock cdn_js %} 59 | 60 | 61 | 62 | 63 | 64 | {% block js %}{% endblock js %} 65 | 66 | {% endblock %} 67 | 68 | 69 | -------------------------------------------------------------------------------- /status/templates/status/health.html: -------------------------------------------------------------------------------- 1 | {% extends "status/base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block content %} 5 | 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block js %} 11 | 12 | {% endblock js %} 13 | -------------------------------------------------------------------------------- /status/templates/status/main.html: -------------------------------------------------------------------------------- 1 | {% extends "status/base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block content %} 5 | 6 | 7 | 8 | 9 | {% endblock %} 10 | 11 | {% block js %} 12 | 13 | {% endblock js %} 14 | -------------------------------------------------------------------------------- /status/templates/status/stats.html: -------------------------------------------------------------------------------- 1 | {% extends "status/base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block content %} 5 | 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block js %} 11 | 12 | {% endblock js %} 13 | -------------------------------------------------------------------------------- /status/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | URLs. 4 | """ 5 | from django.conf.urls import include, url 6 | from django.views.generic import TemplateView 7 | 8 | app_name = 'status' 9 | urlpatterns = [ 10 | url(r'^api/', include('status.api.urls')), 11 | url(r'^$', TemplateView.as_view(template_name='status/main.html')), 12 | url(r'^stats/?$', TemplateView.as_view(template_name='status/stats.html')), 13 | url(r'^health/?$', TemplateView.as_view(template_name='status/health.html')), 14 | ] 15 | -------------------------------------------------------------------------------- /status/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Utils. 4 | """ 5 | 6 | 7 | class FakeChecker(object): 8 | def __getattribute__(self, item): 9 | return lambda *args, **kwargs: None 10 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perdy/django-status/99afa448c1240a8d877ae362beef31077838fa33/tests/__init__.py -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | -r ../requirements.txt 2 | coverage 3 | 4 | nose 5 | prospector 6 | tox 7 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../')) 5 | APP_PATH = os.path.abspath(os.path.dirname(__file__)) 6 | 7 | DEBUG = True 8 | TEMPLATE_DEBUG = DEBUG 9 | 10 | ADMINS = ( 11 | # ('Your Name', 'your_email@example.com'), 12 | ) 13 | 14 | MANAGERS = ADMINS 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 19 | 'NAME': 'test.db', # Or path to database file if using sqlite3. 20 | 'USER': '', # Not used with sqlite3. 21 | 'PASSWORD': '', # Not used with sqlite3. 22 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 23 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 24 | }, 25 | } 26 | 27 | ALLOWED_HOSTS = ['*'] 28 | 29 | TIME_ZONE = 'Europe/Madrid' 30 | 31 | LANGUAGE_CODE = 'en-us' 32 | 33 | SITE_ID = 1 34 | 35 | USE_I18N = True 36 | 37 | USE_L10N = True 38 | 39 | USE_TZ = True 40 | 41 | MEDIA_ROOT = BASE_DIR + '/media/' 42 | 43 | MEDIA_URL = '/media/' 44 | 45 | STATIC_ROOT = BASE_DIR + '/static/' 46 | 47 | STATIC_URL = '/static/' 48 | 49 | STATICFILES_DIRS = ( 50 | APP_PATH + '/static/', 51 | ) 52 | 53 | STATICFILES_FINDERS = ( 54 | 'django.contrib.staticfiles.finders.FileSystemFinder', 55 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 56 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 57 | ) 58 | 59 | SECRET_KEY = '3+&*6eq%(gs1heqnu!fpb%_^3x#s_nh04^7dxa8tgb%meu' 60 | 61 | TEMPLATE_LOADERS = ( 62 | 'django.template.loaders.filesystem.Loader', 63 | 'django.template.loaders.app_directories.Loader', 64 | # 'django.template.loaders.eggs.Loader', 65 | ) 66 | 67 | TEMPLATE_CONTEXT_PROCESSORS = ( 68 | 'django.contrib.auth.context_processors.auth', 69 | "django.core.context_processors.debug", 70 | "django.core.context_processors.i18n", 71 | "django.core.context_processors.media", 72 | # "django.core.context_processors.request", 73 | ) 74 | 75 | MIDDLEWARE_CLASSES = ( 76 | 'django.middleware.common.CommonMiddleware', 77 | 'django.contrib.sessions.middleware.SessionMiddleware', 78 | 'django.middleware.csrf.CsrfViewMiddleware', 79 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 80 | 'django.contrib.messages.middleware.MessageMiddleware', 81 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 82 | ) 83 | 84 | ROOT_URLCONF = 'tests.urls' 85 | 86 | WSGI_APPLICATION = 'tests.wsgi.application' 87 | 88 | TEMPLATE_DIRS = ( 89 | APP_PATH + '/templates/', 90 | ) 91 | 92 | INSTALLED_APPS = ( 93 | 'django.contrib.auth', 94 | 'django.contrib.contenttypes', 95 | 'django.contrib.sessions', 96 | 'django.contrib.sites', 97 | 'django.contrib.messages', 98 | 'django.contrib.staticfiles', 99 | 'django.contrib.admin', 100 | # 'django.contrib.admindocs', 101 | 'status', 102 | ) 103 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{35,36}, lint 3 | 4 | [testenv] 5 | deps = -r{toxinidir}/tests/requirements.txt 6 | setenv = PYTHONPATH = {toxinidir} 7 | commands = python runtests.py --skip-lint 8 | 9 | [testenv:lint] 10 | deps = -r{toxinidir}/tests/requirements.txt 11 | setenv = PYTHONPATH = {toxinidir} 12 | commands = python runtests.py --skip-tests 13 | --------------------------------------------------------------------------------