Giter Site home page Giter Site logo

mwarkentin / django-watchman Goto Github PK

View Code? Open in Web Editor NEW
516.0 11.0 59.0 368 KB

django-watchman exposes a status endpoint for your backing services like databases, caches, etc.

License: BSD 3-Clause "New" or "Revised" License

Makefile 2.16% Python 87.98% HTML 9.55% Dockerfile 0.31%
python django healthcheck monitoring pingdom panopta database cache django-watchman

django-watchman's Introduction

django-watchman

django-watchman exposes a status endpoint for your backing services like databases, caches, etc.

https://mwarkentin-snaps.s3.amazonaws.com/Watchmen_The_One_Thing_Nobody_Says_about_Adrian_Veidt_aka_Ozymandias_2022-03-23_08-36-18.png

Documentation

The full documentation is at http://django-watchman.rtfd.org.

Testimonials

We're in love with django-watchman. External monitoring is a vital part of our service offering. Using django-watchman we can introspect the infrastructure of an application via a secure URL. It's very well written and easy to extend. We've recommended it to many of our clients already.

โ€” Hany Fahim, CEO, VM Farms.

Quickstart

  1. Install django-watchman:

    pip install django-watchman
    
  2. Add watchman to your INSTALLED_APPS setting like this:

    INSTALLED_APPS = (
        ...
        'watchman',
    )
    
  3. Include the watchman URLconf in your project urls.py like this:

    re_path(r'^watchman/', include('watchman.urls')),
    
  4. Start the development server and visit http://127.0.0.1:8000/watchman/ to get a JSON response of your backing service statuses:

    {
        "databases": [
            {
                "default": {
                    "ok": true
                }
            }
        ],
        "caches": [
            {
                "default": {
                    "ok": true
                }
            }
        ],
        "storage": {"ok": true}
    }
    

Pycon Canada Presentation (10 minutes)

https://mwarkentin-snaps.s3.amazonaws.com/Full-stack_Django_application_monitoring_with_django-watchman_Michael_Warkentin_-_YouTube_2022-03-23_08-34-24.png

Features

Human-friendly dashboard

Visit http://127.0.0.1:8000/watchman/dashboard/ to get a human-friendly HTML representation of all of your watchman checks.

Token based authentication

If you want to protect the status endpoint, you can use the WATCHMAN_TOKENS setting. This is a comma-separated list of tokens. When this setting is added, you must pass one of the tokens in as the watchman-token GET parameter:

GET http://127.0.0.1:8000/watchman/?watchman-token=:token

Or by setting the Authorization: WATCHMAN-TOKEN header on the request:

curl -X GET -H "Authorization: WATCHMAN-TOKEN Token=\":token\"" http://127.0.0.1:8000/watchman/

If you want to change the token name, you can set the WATCHMAN_TOKEN_NAME. The value of this setting will be the GET parameter that you must pass in:

WATCHMAN_TOKEN_NAME = 'custom-token-name'

GET http://127.0.0.1:8000/watchman/?custom-token-name=:token

DEPRECATION WARNING: WATCHMAN_TOKEN was replaced by the WATCHMAN_TOKENS setting to support multiple authentication tokens in django-watchman 0.11. It will continue to work until it's removed in django-watchman 1.0.

Custom authentication/authorization

If you want to protect the status endpoint with a customized authentication/authorization decorator, you can add WATCHMAN_AUTH_DECORATOR to your settings. This needs to be a dotted-path to a decorator, and defaults to watchman.decorators.token_required:

WATCHMAN_AUTH_DECORATOR = 'django.contrib.admin.views.decorators.staff_member_required'

Note that the token_required decorator does not protect a view unless WATCHMAN_TOKENS is set in settings.

Custom checks

django-watchman allows you to customize the checks which are run by modifying the WATCHMAN_CHECKS setting. In settings.py:

WATCHMAN_CHECKS = (
    'module.path.to.callable',
    'another.module.path.to.callable',
)

You can also import the watchman.constants to include the DEFAULT_CHECKS and PAID_CHECKS in your settings.py:

from watchman import constants as watchman_constants

WATCHMAN_CHECKS = watchman_constants.DEFAULT_CHECKS + ('module.path.to.callable', )

Checks take no arguments, and must return a dict whose keys are applied to the JSON response.

Use the watchman.decorators.check decorator to capture exceptions:

from watchman.decorators import check

def custom_check():
    return {"custom_check": _custom_check()}

@check
def _custom_check():
    return {"ok": True, "extra_info": "if helpful"}

In the absence of any checks, a 404 is thrown, which is then handled by the json_view decorator.

Run a subset of available checks

A subset of checks may be run, by passing ?check=module.path.to.callable&check=... in the request URL. Only the callables given in the querystring which are also in WATCHMAN_CHECKS should be run, eg:

curl -XGET http://127.0.0.1:8080/watchman/?check=watchman.checks.caches

Skip specific checks

You can skip any number of checks, by passing ?skip=module.path.to.callable&skip=... in the request URL. Only the checks in WATCHMAN_CHECKS which are not in the querystring should be run, eg:

curl -XGET http://127.0.0.1:8080/watchman/?skip=watchman.checks.email

Check a subset of databases or caches

If your application has a large number of databases or caches configured, watchman may open too many connections as it checks each database or cache.

You can set the WATCHMAN_DATABASES or WATCHMAN_CACHES settings in order to override the default set of databases and caches to be monitored.

Ping

If you want to simply check that your application is running and able to handle requests, you can call ping:

GET http://127.0.0.1:8000/watchman/ping/

It will return the text pong with a 200 status code. Calling this doesn't run any of the checks.

Bare status view

If you would like a "bare" status view (one that doesn't report any details, just HTTP 200 if checks pass, and HTTP 500 if any checks fail), you can use the bare_status view by putting the following into urls.py:

import watchman.views
# ...
re_path(r'^status/?$', watchman.views.bare_status),

Django management command

You can also run your checks without starting the webserver and making requests. This can be useful for testing your configuration before enabling a server, checking configuration on worker servers, etc. Run the management command like so:

python manage.py watchman

By default, successful checks will not print any output. If all checks pass successfully, the exit code will be 0. If a check fails, the exit code will be 1, and the error message including stack trace will be printed to stderr.

If you'd like to see output for successful checks as well, set verbosity to 2 or higher:

python manage.py watchman -v 2
{"storage": {"ok": true}}
{"caches": [{"default": {"ok": true}}]}
{"databases": [{"default": {"ok": true}}]}

If you'd like to run a subset of checks, use -c and a comma-separated list of python module paths:

python manage.py watchman -c watchman.checks.caches,watchman.checks.databases -v 2
{"caches": [{"default": {"ok": true}}]}
{"databases": [{"default": {"ok": true}}]}

If you'd like to skip certain checks, use -s and a comma-separated list of python module paths:

python manage.py watchman -s watchman.checks.caches,watchman.checks.databases -v 2
{"storage": {"ok": true}}

Use -h to see a full list of options:

python manage.py watchman -h

X-Watchman-Version response header

Watchman can return the version of watchman which is running to help you keep track of whether or not your sites are using an up-to-date version. This is disabled by default to prevent any unintended information leakage for websites without authentication. To enable, update the EXPOSE_WATCHMAN_VERSION setting:

EXPOSE_WATCHMAN_VERSION = True

Custom response code

By default, watchman will return a 500 HTTP response code, even if there's a failing check. You can specify a different response code for failing checks using the WATCHMAN_ERROR_CODE setting:

WATCHMAN_ERROR_CODE = 200

Logging

watchman includes log messages using a logger called watchman. You can configure this by configuring the LOGGING section of your Django settings file.

Here is a simple example that would log to the console:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'watchman': {
            'handlers': ['console'],
            'level': 'DEBUG',
        },
    },
}

More information is available in the Django documentation.

APM (Datadog, New Relic)

If you're using APM and watchman is being often hit for health checks (such as an ELB on AWS), you will find some stats based on averages will be affected (average transaction time, apdex, etc):

You can disable APM instrumentation for watchman by using the WATCHMAN_DISABLE_APM setting:

WATCHMAN_DISABLE_APM = True

This currently supports the following agents:

  • Datadog
  • New Relic

Please open an issue if there's another APM you use which is being affected.

Available checks

caches

For each cache in django.conf.settings.CACHES:

  • Set a test cache item
  • Get test item
  • Delete test item

databases

For each database in django.conf.settings.DATABASES:

  • Verify connection by calling connections[database].introspection.table_names()

email

Send a test email to [email protected] using django.core.mail.send_mail.

If you're using a 3rd party mail provider, this check could end up costing you money, depending how aggressive you are with your monitoring. For this reason, this check is not enabled by default.

For reference, if you were using Mandrill, and hitting your watchman endpoint once per minute, this would cost you ~$5.60/month.

Custom Settings

  • WATCHMAN_EMAIL_SENDER (default: [email protected]): Specify an email to be the sender of the test email
  • WATCHMAN_EMAIL_RECIPIENTS (default: [[email protected]]): Specify a list of email addresses to send the test email
  • WATCHMAN_EMAIL_HEADERS (default: {}): Specify a dict of custom headers to be added to the test email

storage

Using django.core.files.storage.default_storage:

  • Write a test file
  • Check the test file's size
  • Read the test file's contents
  • Delete the test file

By default the test file gets written on the root of the django MEDIA_ROOT. If for whatever reasons this path is not writable by the user that runs the application you can override it by setting WATCHMAN_STORAGE_PATH to a specific path. Remember that this must be within the MEDIA_ROOT, which by default is your project root. In settings.py:

WATCHMAN_STORAGE_PATH = "/path_to_your_app/foo/bar/"

If the MEDIA_ROOT is already defined:

from os.path import join as joinpath
WATCHMAN_STORAGE_PATH = joinpath(MEDIA_ROOT, "foo/bar")

Default checks

By default, django-watchman will run checks against your databases (watchman.checks.databases), caches (watchman.checks.caches), and storage (watchman.checks.storage).

Paid checks

Paid checks are checks which may cost you money if they are run regularly.

Currently there is only one "paid" check - watchman.checks.email. Many times email is sent using managed services like SendGrid or Mailgun. You can enable it by setting the WATCHMAN_ENABLE_PAID_CHECKS to True, or by overriding the WATCHMAN_CHECKS setting.

Trying it out with Docker

A sample project is available along with a Dockerfile to make it easy to try out django-watchman. It includes examples of how to write simple custom checks.

One of the custom checks will always fail, so if you want to see what responses look like with 100% succeeding checks, add ?skip=sample_project.checks.fail_custom_check

Requirements

Instructions

  1. Build and run the Docker image with the current local code: make run
  2. Visit watchman json endpoint in your browser: http://127.0.0.1:8000/watchman/
  3. Visit watchman json endpoint in your browser: http://127.0.0.1:8000/watchman/?skip=sample_project.checks.fail_custom_check
  4. Visit watchman dashboard in your browser: http://127.0.0.1:8000/watchman/dashboard/
  5. Visit watchman dashboard in your browser: http://127.0.0.1:8000/watchman/dashboard/?skip=sample_project.checks.fail_custom_check
  6. Visit watchman ping in your browser: http://127.0.0.1:8000/watchman/ping/
  7. Visit watchman bare status in your browser: http://127.0.0.1:8000/watchman/bare/

django-watchman's People

Contributors

benwebber avatar blag avatar coredumperror avatar cristianemoyano avatar daniego avatar dependabot-preview[bot] avatar dhoffman34 avatar dominik-horb-umg avatar eduardocardoso avatar fladi avatar jayh5 avatar jbkahn avatar jmaroeder avatar jonespm avatar justinsacbibit avatar kezabelle avatar mwarkentin avatar orthographic-pedant avatar pre-commit-ci[bot] avatar robatwave avatar ryanwilsonperkin avatar saily avatar tirkarthi avatar tisdall avatar ulope avatar xfxf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-watchman's Issues

Set timeout for watchman checks

Currently there's no way to set timeouts for checks - so you'll get a generic error if your request times out on it's own:

[172.16.12.72] Functional test: curl "0.0.0.0:8000/watchman/?watchman-token=WATCHMAN_TOKEN&check=watchman.checks.caches&check=watchman.checks.databases" -H "Host:app.waveapps.com" --max-time 30
[172.16.12.72] curl: (28) Operation timed out after 30000 milliseconds with 0 bytes received
[172.16.12.72] Functional test: curl "0.0.0.0:80/watchman/?watchman-token=WATCHMAN_TOKEN&check=watchman.checks.caches&check=watchman.checks.databases" -H "Host:app.waveapps.com" --max-time 30
[172.16.12.72] curl: (7) couldn't connect to host

It'd be nice if we could set a timeout on the checks so that we could see which specific check took too long.

Questions

  • Should the timeout be global, or per check? Both?
  • Can you customize it with query params, CLI options, etc.?

If you have a lot of DBs, watchman can open a lot of connections

Ran into this issue when increasing from 64 -> 1024 shards:

1024 shards:

{"databases": [{"default": {"ok": true}}, {"shard_0001": {"ok": true}}, {"shard_0002": {"ok": true}}, {"shard_0003": {"ok": true}}, {"shard_0004": {"ok": true}}, {"shard_0005": {"ok": true}}, {"shard_0006": {"ok": true}}, {"shard_0007": {"ok": true}}, {"shard_0008": {"ok": true}}, {"shard_0009": {"ok": true}}, {"shard_0010": {"ok": true}}, {"shard_0011": {"ok": true}}, {"shard_0012": {"ok": true}}, {"shard_0013": {"ok": true}}, {"shard_0014": {"ok": true}}, {"shard_0015": {"ok": true}}, {"shard_0016": {"ok": true}}, {"shard_0017": {"ok": true}}, {"shard_0018": {"ok": true}}, {"shard_0019": {"ok": true}}, {"shard_0020": {"ok": true}}, {"shard_0021": {"ok": true}}, {"shard_0022": {"ok": true}}, {"shard_0023": {"ok": true}}, {"shard_0024": {"ok": true}}, {"shard_0025": {"ok": true}}, {"shard_0026": {"ok": true}}, {"shard_0027": {"ok": true}}, {"shard_0028": {"ok": true}}, {"shard_0029": {"ok": true}}, {"shard_0030": {"ok": true}}, {"shard_0031": {"ok": true}}, {"shard_0032": {"ok": true}}, {"shard_0033": {"ok": true}}, {"shard_0034": {"ok": true}}, {"shard_0035": {"ok": true}}, {"shard_0036": {"ok": true}}, {"shard_0037": {"ok": true}}, {"shard_0038": {"ok": true}}, {"shard_0039": {"ok": true}}, {"shard_0040": {"ok": true}}, {"shard_0041": {"ok": true}}, {"shard_0042": {"ok": true}}, {"shard_0043": {"ok": true}}, {"shard_0044": {"ok": true}}, {"shard_0045": {"ok": true}}, {"shard_0046": {"ok": true}}, {"shard_0047": {"ok": true}}, {"shard_0048": {"ok": true}}, {"shard_0049": {"ok": true}}, {"shard_0050": {"ok": true}}, {"shard_0051": {"ok": true}}, {"shard_0052": {"ok": true}}, {"shard_0053": {"ok": true}}, {"shard_0054": {"ok": true}}, {"shard_0055": {"ok": true}}, {"shard_0056": {"ok": true}}, {"shard_0057": {"ok": true}}, {"shard_0058": {"ok": true}}, {"shard_0059": {"ok": true}}, {"shard_0060": {"ok": true}}, {"shard_0061": {"ok": true}}, {"shard_0062": {"ok": true}}, {"shard_0063": {"ok": true}}, {"shard_0064": {"ok": true}}, {"shard_0065": {"ok": true}}, {"shard_0066": {"ok": true}}, {"shard_0067": {"ok": true}}, {"shard_0068": {"ok": true}}, {"shard_0069": {"ok": true}}, {"shard_0070": {"ok": true}}, {"shard_0071": {"ok": true}}, {"shard_0072": {"ok": true}}, {"shard_0073": {"ok": true}}, {"shard_0074": {"ok": true}}, {"shard_0075": {"ok": true}}, {"shard_0076": {"ok": true}}, {"shard_0077": {"ok": true}}, {"shard_0078": {"ok": true}}, {"shard_0079": {"ok": true}}, {"shard_0080": {"ok": true}}, {"shard_0081": {"ok": true}}, {"shard_0082": {"ok": true}}, {"shard_0083": {"ok": true}}, {"shard_0084": {"ok": true}}, {"shard_0085": {"ok": true}}, {"shard_0086": {"ok": true}}, {"shard_0087": {"ok": true}}, {"shard_0088": {"ok": true}}, {"shard_0089": {"ok": true}}, {"shard_0090": {"ok": true}}, {"shard_0091": {"ok": true}}, {"shard_0092": {"ok": true}}, {"shard_0093": {"ok": true}}, {"shard_0094": {"ok": true}}, {"shard_0095": {"ok": true}}, {"shard_0096": {"ok": true}}, {"shard_0097": {"ok": true}}, {"shard_0098": {"ok": true}}, {"shard_0099": {"ok": true}}, {"shard_0100": {"ok": true}}, {"shard_0101": {"ok": true}}, {"shard_0102": {"ok": true}}, {"shard_0103": {"ok": true}}, {"shard_0104": {"ok": true}}, {"shard_0105": {"ok": true}}, {"shard_0106": {"ok": true}}, {"shard_0107": {"ok": true}}, {"shard_0108": {"ok": true}}, {"shard_0109": {"ok": true}}, {"shard_0110": {"ok": true}}, {"shard_0111": {"ok": true}}, {"shard_0112": {"ok": true}}, {"shard_0113": {"ok": true}}, {"shard_0114": {"ok": true}}, {"shard_0115": {"ok": true}}, {"shard_0116": {"ok": true}}, {"shard_0117": {"ok": true}}, {"shard_0118": {"ok": true}}, {"shard_0119": {"ok": true}}, {"shard_0120": {"ok": true}}, {"shard_0121": {"ok": true}}, {"shard_0122": {"ok": true}}, {"shard_0123": {"ok": true}}, {"shard_0124": {"ok": true}}, {"shard_0125": {"ok": true}}, {"shard_0126": {"ok": true}}, {"shard_0127": {"ok": true}}, {"shard_0128": {"ok": true}}, {"shard_0129": {"ok": true}}, {"shard_0130": {"ok": true}}, {"shard_0131": {"ok": true}}, {"shard_0132": {"ok": true}}, {"shard_0133": {"ok": true}}, {"shard_0134": {"ok": true}}, {"shard_0135": {"ok": true}}, {"shard_0136": {"ok": true}}, {"shard_0137": {"ok": true}}, {"shard_0138": {"ok": true}}, {"shard_0139": {"ok": true}}, {"shard_0140": {"ok": true}}, {"shard_0141": {"ok": true}}, {"shard_0142": {"ok": true}}, {"shard_0143": {"ok": true}}, {"shard_0144": {"ok": true}}, {"shard_0145": {"ok": true}}, {"shard_0146": {"ok": true}}, {"shard_0147": {"ok": true}}, {"shard_0148": {"ok": true}}, {"shard_0149": {"ok": true}}, {"shard_0150": {"ok": true}}, {"shard_0151": {"ok": true}}, {"shard_0152": {"ok": true}}, {"shard_0153": {"ok": true}}, {"shard_0154": {"ok": true}}, {"shard_0155": {"ok": true}}, {"shard_0156": {"ok": true}}, {"shard_0157": {"ok": true}}, {"shard_0158": {"ok": true}}, {"shard_0159": {"ok": true}}, {"shard_0160": {"ok": true}}, {"shard_0161": {"ok": true}}, {"shard_0162": {"ok": true}}, {"shard_0163": {"ok": true}}, {"shard_0164": {"ok": true}}, {"shard_0165": {"ok": true}}, {"shard_0166": {"ok": true}}, {"shard_0167": {"ok": true}}, {"shard_0168": {"ok": true}}, {"shard_0169": {"ok": true}}, {"shard_0170": {"ok": true}}, {"shard_0171": {"ok": true}}, {"shard_0172": {"ok": true}}, {"shard_0173": {"ok": true}}, {"shard_0174": {"ok": true}}, {"shard_0175": {"ok": true}}, {"shard_0176": {"ok": true}}, {"shard_0177": {"ok": true}}, {"shard_0178": {"ok": true}}, {"shard_0179": {"ok": true}}, {"shard_0180": {"ok": true}}, {"shard_0181": {"ok": true}}, {"shard_0182": {"ok": true}}, {"shard_0183": {"ok": true}}, {"shard_0184": {"ok": true}}, {"shard_0185": {"ok": true}}, {"shard_0186": {"ok": true}}, {"shard_0187": {"ok": true}}, {"shard_0188": {"ok": true}}, {"shard_0189": {"ok": true}}, {"shard_0190": {"ok": true}}, {"shard_0191": {"ok": true}}, {"shard_0192": {"ok": true}}, {"shard_0193": {"ok": true}}, {"shard_0194": {"ok": true}}, {"shard_0195": {"ok": true}}, {"shard_0196": {"ok": true}}, {"shard_0197": {"ok": true}}, {"shard_0198": {"ok": true}}, {"shard_0199": {"ok": true}}, {"shard_0200": {"ok": true}}, {"shard_0201": {"ok": true}}, {"shard_0202": {"ok": true}}, {"shard_0203": {"ok": true}}, {"shard_0204": {"ok": true}}, {"shard_0205": {"ok": true}}, {"shard_0206": {"ok": true}}, {"shard_0207": {"ok": true}}, {"shard_0208": {"ok": true}}, {"shard_0209": {"ok": true}}, {"shard_0210": {"ok": true}}, {"shard_0211": {"ok": true}}, {"shard_0212": {"ok": true}}, {"shard_0213": {"ok": true}}, {"shard_0214": {"ok": true}}, {"shard_0215": {"ok": true}}, {"shard_0216": {"ok": true}}, {"shard_0217": {"ok": true}}, {"shard_0218": {"ok": true}}, {"shard_0219": {"ok": true}}, {"shard_0220": {"ok": true}}, {"shard_0221": {"ok": true}}, {"shard_0222": {"ok": true}}, {"shard_0223": {"ok": true}}, {"shard_0224": {"ok": true}}, {"shard_0225": {"ok": true}}, {"shard_0226": {"ok": true}}, {"shard_0227": {"ok": true}}, {"shard_0228": {"ok": true}}, {"shard_0229": {"ok": true}}, {"shard_0230": {"ok": true}}, {"shard_0231": {"ok": true}}, {"shard_0232": {"ok": true}}, {"shard_0233": {"ok": true}}, {"shard_0234": {"ok": true}}, {"shard_0235": {"ok": true}}, {"shard_0236": {"ok": true}}, {"shard_0237": {"ok": true}}, {"shard_0238": {"ok": true}}, {"shard_0239": {"ok": true}}, {"shard_0240": {"ok": true}}, {"shard_0241": {"ok": true}}, {"shard_0242": {"ok": true}}, {"shard_0243": {"ok": true}}, {"shard_0244": {"ok": true}}, {"shard_0245": {"ok": true}}, {"shard_0246": {"ok": true}}, {"shard_0247": {"ok": true}}, {"shard_0248": {"ok": true}}, {"shard_0249": {"ok": true}}, {"shard_0250": {"ok": true}}, {"shard_0251": {"ok": true}}, {"shard_0252": {"ok": true}}, {"shard_0253": {"ok": true}}, {"shard_0254": {"ok": true}}, {"shard_0255": {"ok": true}}, {"shard_0256": {"ok": true}}, {"shard_0257": {"ok": true}}, {"shard_0258": {"ok": true}}, {"shard_0259": {"ok": true}}, {"shard_0260": {"ok": true}}, {"shard_0261": {"ok": true}}, {"shard_0262": {"ok": true}}, {"shard_0263": {"ok": true}}, {"shard_0264": {"ok": true}}, {"shard_0265": {"ok": true}}, {"shard_0266": {"ok": true}}, {"shard_0267": {"ok": true}}, {"shard_0268": {"ok": true}}, {"shard_0269": {"ok": true}}, {"shard_0270": {"ok": true}}, {"shard_0271": {"ok": true}}, {"shard_0272": {"ok": true}}, {"shard_0273": {"ok": true}}, {"shard_0274": {"ok": true}}, {"shard_0275": {"ok": true}}, {"shard_0276": {"ok": true}}, {"shard_0277": {"ok": true}}, {"shard_0278": {"ok": true}}, {"shard_0279": {"ok": true}}, {"shard_0280": {"ok": true}}, {"shard_0281": {"ok": true}}, {"shard_0282": {"ok": true}}, {"shard_0283": {"ok": true}}, {"shard_0284": {"ok": true}}, {"shard_0285": {"ok": true}}, {"shard_0286": {"ok": true}}, {"shard_0287": {"ok": true}}, {"shard_0288": {"ok": true}}, {"shard_0289": {"ok": true}}, {"shard_0290": {"ok": true}}, {"shard_0291": {"ok": true}}, {"shard_0292": {"ok": true}}, {"shard_0293": {"ok": true}}, {"shard_0294": {"ok": true}}, {"shard_0295": {"ok": true}}, {"shard_0296": {"ok": true}}, {"shard_0297": {"ok": true}}, {"shard_0298": {"ok": true}}, {"shard_0299": {"ok": true}}, {"shard_0300": {"ok": true}}, {"shard_0301": {"ok": true}}, {"shard_0302": {"ok": true}}, {"shard_0303": {"ok": true}}, {"shard_0304": {"ok": true}}, {"shard_0305": {"ok": true}}, {"shard_0306": {"ok": true}}, {"shard_0307": {"ok": true}}, {"shard_0308": {"ok": true}}, {"shard_0309": {"ok": true}}, {"shard_0310": {"ok": true}}, {"shard_0311": {"ok": true}}, {"shard_0312": {"ok": true}}, {"shard_0313": {"ok": true}}, {"shard_0314": {"ok": true}}, {"shard_0315": {"ok": true}}, {"shard_0316": {"ok": true}}, {"shard_0317": {"ok": true}}, {"shard_0318": {"ok": true}}, {"shard_0319": {"ok": true}}, {"shard_0320": {"ok": true}}, {"shard_0321": {"ok": true}}, {"shard_0322": {"ok": true}}, {"shard_0323": {"ok": true}}, {"shard_0324": {"ok": true}}, {"shard_0325": {"ok": true}}, {"shard_0326": {"ok": true}}, {"shard_0327": {"ok": true}}, {"shard_0328": {"ok": true}}, {"shard_0329": {"ok": true}}, {"shard_0330": {"ok": true}}, {"shard_0331": {"ok": true}}, {"shard_0332": {"ok": true}}, {"shard_0333": {"ok": true}}, {"shard_0334": {"ok": true}}, {"shard_0335": {"ok": true}}, {"shard_0336": {"ok": true}}, {"shard_0337": {"ok": true}}, {"shard_0338": {"ok": true}}, {"shard_0339": {"ok": true}}, {"shard_0340": {"ok": true}}, {"shard_0341": {"ok": true}}, {"shard_0342": {"ok": true}}, {"shard_0343": {"ok": true}}, {"shard_0344": {"ok": true}}, {"shard_0345": {"ok": true}}, {"shard_0346": {"ok": true}}, {"shard_0347": {"ok": true}}, {"shard_0348": {"ok": true}}, {"shard_0349": {"ok": true}}, {"shard_0350": {"ok": true}}, {"shard_0351": {"ok": true}}, {"shard_0352": {"ok": true}}, {"shard_0353": {"ok": true}}, {"shard_0354": {"ok": true}}, {"shard_0355": {"ok": true}}, {"shard_0356": {"ok": true}}, {"shard_0357": {"ok": true}}, {"shard_0358": {"ok": true}}, {"shard_0359": {"ok": true}}, {"shard_0360": {"ok": true}}, {"shard_0361": {"ok": true}}, {"shard_0362": {"ok": true}}, {"shard_0363": {"ok": true}}, {"shard_0364": {"ok": true}}, {"shard_0365": {"ok": true}}, {"shard_0366": {"ok": true}}, {"shard_0367": {"ok": true}}, {"shard_0368": {"ok": true}}, {"shard_0369": {"ok": true}}, {"shard_0370": {"ok": true}}, {"shard_0371": {"ok": true}}, {"shard_0372": {"ok": true}}, {"shard_0373": {"ok": true}}, {"shard_0374": {"ok": true}}, {"shard_0375": {"ok": true}}, {"shard_0376": {"ok": true}}, {"shard_0377": {"ok": true}}, {"shard_0378": {"ok": true}}, {"shard_0379": {"ok": true}}, {"shard_0380": {"ok": true}}, {"shard_0381": {"ok": true}}, {"shard_0382": {"ok": true}}, {"shard_0383": {"ok": true}}, {"shard_0384": {"ok": true}}, {"shard_0385": {"ok": true}}, {"shard_0386": {"ok": true}}, {"shard_0387": {"ok": true}}, {"shard_0388": {"ok": true}}, {"shard_0389": {"ok": true}}, {"shard_0390": {"ok": true}}, {"shard_0391": {"ok": true}}, {"shard_0392": {"ok": true}}, {"shard_0393": {"ok": true}}, {"shard_0394": {"ok": true}}, {"shard_0395": {"ok": true}}, {"shard_0396": {"ok": true}}, {"shard_0397": {"ok": true}}, {"shard_0398": {"ok": true}}, {"shard_0399": {"ok": true}}, {"shard_0400": {"ok": true}}, {"shard_0401": {"ok": true}}, {"shard_0402": {"ok": true}}, {"shard_0403": {"ok": true}}, {"shard_0404": {"ok": true}}, {"shard_0405": {"ok": true}}, {"shard_0406": {"ok": true}}, {"shard_0407": {"ok": true}}, {"shard_0408": {"ok": true}}, {"shard_0409": {"ok": true}}, {"shard_0410": {"ok": true}}, {"shard_0411": {"ok": true}}, {"shard_0412": {"ok": true}}, {"shard_0413": {"ok": true}}, {"shard_0414": {"ok": true}}, {"shard_0415": {"ok": true}}, {"shard_0416": {"ok": true}}, {"shard_0417": {"ok": true}}, {"shard_0418": {"ok": true}}, {"shard_0419": {"ok": true}}, {"shard_0420": {"ok": true}}, {"shard_0421": {"ok": true}}, {"shard_0422": {"ok": true}}, {"shard_0423": {"ok": true}}, {"shard_0424": {"ok": true}}, {"shard_0425": {"ok": true}}, {"shard_0426": {"ok": true}}, {"shard_0427": {"ok": true}}, {"shard_0428": {"ok": true}}, {"shard_0429": {"ok": true}}, {"shard_0430": {"ok": true}}, {"shard_0431": {"ok": true}}, {"shard_0432": {"ok": true}}, {"shard_0433": {"ok": true}}, {"shard_0434": {"ok": true}}, {"shard_0435": {"ok": true}}, {"shard_0436": {"ok": true}}, {"shard_0437": {"ok": true}}, {"shard_0438": {"ok": true}}, {"shard_0439": {"ok": true}}, {"shard_0440": {"ok": true}}, {"shard_0441": {"ok": true}}, {"shard_0442": {"ok": true}}, {"shard_0443": {"ok": true}}, {"shard_0444": {"ok": true}}, {"shard_0445": {"ok": true}}, {"shard_0446": {"ok": true}}, {"shard_0447": {"ok": true}}, {"shard_0448": {"ok": true}}, {"shard_0449": {"ok": true}}, {"shard_0450": {"ok": true}}, {"shard_0451": {"ok": true}}, {"shard_0452": {"ok": true}}, {"shard_0453": {"ok": true}}, {"shard_0454": {"ok": true}}, {"shard_0455": {"ok": true}}, {"shard_0456": {"ok": true}}, {"shard_0457": {"ok": true}}, {"shard_0458": {"ok": true}}, {"shard_0459": {"ok": true}}, {"shard_0460": {"ok": true}}, {"shard_0461": {"ok": true}}, {"shard_0462": {"ok": true}}, {"shard_0463": {"ok": true}}, {"shard_0464": {"ok": true}}, {"shard_0465": {"ok": true}}, {"shard_0466": {"ok": true}}, {"shard_0467": {"ok": true}}, {"shard_0468": {"ok": true}}, {"shard_0469": {"ok": true}}, {"shard_0470": {"ok": true}}, {"shard_0471": {"ok": true}}, {"shard_0472": {"ok": true}}, {"shard_0473": {"ok": true}}, {"shard_0474": {"ok": true}}, {"shard_0475": {"ok": true}}, {"shard_0476": {"ok": true}}, {"shard_0477": {"ok": true}}, {"shard_0478": {"ok": true}}, {"shard_0479": {"ok": true}}, {"shard_0480": {"ok": true}}, {"shard_0481": {"ok": true}}, {"shard_0482": {"ok": true}}, {"shard_0483": {"ok": true}}, {"shard_0484": {"ok": true}}, {"shard_0485": {"ok": true}}, {"shard_0486": {"ok": true}}, {"shard_0487": {"ok": true}}, {"shard_0488": {"ok": true}}, {"shard_0489": {"ok": true}}, {"shard_0490": {"ok": true}}, {"shard_0491": {"ok": true}}, {"shard_0492": {"ok": true}}, {"shard_0493": {"ok": true}}, {"shard_0494": {"ok": true}}, {"shard_0495": {"ok": true}}, {"shard_0496": {"ok": true}}, {"shard_0497": {"ok": true}}, {"shard_0498": {"ok": true}}, {"shard_0499": {"ok": true}}, {"shard_0500": {"ok": true}}, {"shard_0501": {"ok": true}}, {"shard_0502": {"ok": true}}, {"shard_0503": {"ok": true}}, {"shard_0504": {"ok": true}}, {"shard_0505": {"ok": true}}, {"shard_0506": {"ok": true}}, {"shard_0507": {"ok": true}}, {"shard_0508": {"ok": true}}, {"shard_0509": {"ok": true}}, {"shard_0510": {"ok": true}}, {"shard_0511": {"ok": true}}, {"shard_0512": {"ok": true}}, {"shard_0513": {"ok": true}}, {"shard_0514": {"ok": true}}, {"shard_0515": {"ok": true}}, {"shard_0516": {"ok": true}}, {"shard_0517": {"ok": true}}, {"shard_0518": {"ok": true}}, {"shard_0519": {"ok": true}}, {"shard_0520": {"ok": true}}, {"shard_0521": {"ok": true}}, {"shard_0522": {"ok": true}}, {"shard_0523": {"ok": true}}, {"shard_0524": {"ok": true}}, {"shard_0525": {"ok": true}}, {"shard_0526": {"ok": true}}, {"shard_0527": {"ok": true}}, {"shard_0528": {"ok": true}}, {"shard_0529": {"ok": true}}, {"shard_0530": {"ok": true}}, {"shard_0531": {"ok": true}}, {"shard_0532": {"ok": true}}, {"shard_0533": {"ok": true}}, {"shard_0534": {"ok": true}}, {"shard_0535": {"ok": true}}, {"shard_0536": {"ok": true}}, {"shard_0537": {"ok": true}}, {"shard_0538": {"ok": true}}, {"shard_0539": {"ok": true}}, {"shard_0540": {"ok": true}}, {"shard_0541": {"ok": true}}, {"shard_0542": {"ok": true}}, {"shard_0543": {"ok": true}}, {"shard_0544": {"ok": true}}, {"shard_0545": {"ok": true}}, {"shard_0546": {"ok": true}}, {"shard_0547": {"ok": true}}, {"shard_0548": {"ok": true}}, {"shard_0549": {"ok": true}}, {"shard_0550": {"ok": true}}, {"shard_0551": {"ok": true}}, {"shard_0552": {"ok": true}}, {"shard_0553": {"ok": true}}, {"shard_0554": {"ok": true}}, {"shard_0555": {"ok": true}}, {"shard_0556": {"ok": true}}, {"shard_0557": {"ok": true}}, {"shard_0558": {"ok": true}}, {"shard_0559": {"ok": true}}, {"shard_0560": {"ok": true}}, {"shard_0561": {"ok": true}}, {"shard_0562": {"ok": true}}, {"shard_0563": {"ok": true}}, {"shard_0564": {"ok": true}}, {"shard_0565": {"ok": true}}, {"shard_0566": {"ok": true}}, {"shard_0567": {"ok": true}}, {"shard_0568": {"ok": true}}, {"shard_0569": {"ok": true}}, {"shard_0570": {"ok": true}}, {"shard_0571": {"ok": true}}, {"shard_0572": {"ok": true}}, {"shard_0573": {"ok": true}}, {"shard_0574": {"ok": true}}, {"shard_0575": {"ok": true}}, {"shard_0576": {"ok": true}}, {"shard_0577": {"ok": true}}, {"shard_0578": {"ok": true}}, {"shard_0579": {"ok": true}}, {"shard_0580": {"ok": true}}, {"shard_0581": {"ok": true}}, {"shard_0582": {"ok": true}}, {"shard_0583": {"ok": true}}, {"shard_0584": {"ok": true}}, {"shard_0585": {"ok": true}}, {"shard_0586": {"ok": true}}, {"shard_0587": {"ok": true}}, {"shard_0588": {"ok": true}}, {"shard_0589": {"ok": true}}, {"shard_0590": {"ok": true}}, {"shard_0591": {"ok": true}}, {"shard_0592": {"ok": true}}, {"shard_0593": {"ok": true}}, {"shard_0594": {"ok": true}}, {"shard_0595": {"ok": true}}, {"shard_0596": {"ok": true}}, {"shard_0597": {"ok": true}}, {"shard_0598": {"ok": true}}, {"shard_0599": {"ok": true}}, {"shard_0600": {"ok": true}}, {"shard_0601": {"ok": true}}, {"shard_0602": {"ok": true}}, {"shard_0603": {"ok": true}}, {"shard_0604": {"ok": true}}, {"shard_0605": {"ok": true}}, {"shard_0606": {"ok": true}}, {"shard_0607": {"ok": true}}, {"shard_0608": {"ok": true}}, {"shard_0609": {"ok": true}}, {"shard_0610": {"ok": true}}, {"shard_0611": {"ok": true}}, {"shard_0612": {"ok": true}}, {"shard_0613": {"ok": true}}, {"shard_0614": {"ok": true}}, {"shard_0615": {"ok": true}}, {"shard_0616": {"ok": true}}, {"shard_0617": {"ok": true}}, {"shard_0618": {"ok": true}}, {"shard_0619": {"ok": true}}, {"shard_0620": {"ok": true}}, {"shard_0621": {"ok": true}}, {"shard_0622": {"ok": true}}, {"shard_0623": {"ok": true}}, {"shard_0624": {"ok": true}}, {"shard_0625": {"ok": true}}, {"shard_0626": {"ok": true}}, {"shard_0627": {"ok": true}}, {"shard_0628": {"ok": true}}, {"shard_0629": {"ok": true}}, {"shard_0630": {"ok": true}}, {"shard_0631": {"ok": true}}, {"shard_0632": {"ok": true}}, {"shard_0633": {"ok": true}}, {"shard_0634": {"ok": true}}, {"shard_0635": {"ok": true}}, {"shard_0636": {"ok": true}}, {"shard_0637": {"ok": true}}, {"shard_0638": {"ok": true}}, {"shard_0639": {"ok": true}}, {"shard_0640": {"ok": true}}, {"shard_0641": {"ok": true}}, {"shard_0642": {"ok": true}}, {"shard_0643": {"ok": true}}, {"shard_0644": {"ok": true}}, {"shard_0645": {"ok": true}}, {"shard_0646": {"ok": true}}, {"shard_0647": {"ok": true}}, {"shard_0648": {"ok": true}}, {"shard_0649": {"ok": true}}, {"shard_0650": {"ok": true}}, {"shard_0651": {"ok": true}}, {"shard_0652": {"ok": true}}, {"shard_0653": {"ok": true}}, {"shard_0654": {"ok": true}}, {"shard_0655": {"ok": true}}, {"shard_0656": {"ok": true}}, {"shard_0657": {"ok": true}}, {"shard_0658": {"ok": true}}, {"shard_0659": {"ok": true}}, {"shard_0660": {"ok": true}}, {"shard_0661": {"ok": true}}, {"shard_0662": {"ok": true}}, {"shard_0663": {"ok": true}}, {"shard_0664": {"ok": true}}, {"shard_0665": {"ok": true}}, {"shard_0666": {"ok": true}}, {"shard_0667": {"ok": true}}, {"shard_0668": {"ok": true}}, {"shard_0669": {"ok": true}}, {"shard_0670": {"ok": true}}, {"shard_0671": {"ok": true}}, {"shard_0672": {"ok": true}}, {"shard_0673": {"ok": true}}, {"shard_0674": {"ok": true}}, {"shard_0675": {"ok": true}}, {"shard_0676": {"ok": true}}, {"shard_0677": {"ok": true}}, {"shard_0678": {"ok": true}}, {"shard_0679": {"ok": true}}, {"shard_0680": {"ok": true}}, {"shard_0681": {"ok": true}}, {"shard_0682": {"ok": true}}, {"shard_0683": {"ok": true}}, {"shard_0684": {"ok": true}}, {"shard_0685": {"ok": true}}, {"shard_0686": {"ok": true}}, {"shard_0687": {"ok": true}}, {"shard_0688": {"ok": true}}, {"shard_0689": {"ok": true}}, {"shard_0690": {"ok": true}}, {"shard_0691": {"ok": true}}, {"shard_0692": {"ok": true}}, {"shard_0693": {"ok": true}}, {"shard_0694": {"ok": true}}, {"shard_0695": {"ok": true}}, {"shard_0696": {"ok": true}}, {"shard_0697": {"ok": true}}, {"shard_0698": {"ok": true}}, {"shard_0699": {"ok": true}}, {"shard_0700": {"ok": true}}, {"shard_0701": {"ok": true}}, {"shard_0702": {"ok": true}}, {"shard_0703": {"ok": true}}, {"shard_0704": {"ok": true}}, {"shard_0705": {"ok": true}}, {"shard_0706": {"ok": true}}, {"shard_0707": {"ok": true}}, {"shard_0708": {"ok": true}}, {"shard_0709": {"ok": true}}, {"shard_0710": {"ok": true}}, {"shard_0711": {"ok": true}}, {"shard_0712": {"ok": true}}, {"shard_0713": {"ok": true}}, {"shard_0714": {"ok": true}}, {"shard_0715": {"ok": true}}, {"shard_0716": {"ok": true}}, {"shard_0717": {"ok": true}}, {"shard_0718": {"ok": true}}, {"shard_0719": {"ok": true}}, {"shard_0720": {"ok": true}}, {"shard_0721": {"ok": true}}, {"shard_0722": {"ok": true}}, {"shard_0723": {"ok": true}}, {"shard_0724": {"ok": true}}, {"shard_0725": {"ok": true}}, {"shard_0726": {"ok": true}}, {"shard_0727": {"ok": true}}, {"shard_0728": {"ok": true}}, {"shard_0729": {"ok": true}}, {"shard_0730": {"ok": true}}, {"shard_0731": {"ok": true}}, {"shard_0732": {"ok": true}}, {"shard_0733": {"ok": true}}, {"shard_0734": {"ok": true}}, {"shard_0735": {"ok": true}}, {"shard_0736": {"ok": true}}, {"shard_0737": {"ok": true}}, {"shard_0738": {"ok": true}}, {"shard_0739": {"ok": true}}, {"shard_0740": {"ok": true}}, {"shard_0741": {"ok": true}}, {"shard_0742": {"ok": true}}, {"shard_0743": {"ok": true}}, {"shard_0744": {"ok": true}}, {"shard_0745": {"ok": true}}, {"shard_0746": {"ok": true}}, {"shard_0747": {"ok": true}}, {"shard_0748": {"ok": true}}, {"shard_0749": {"ok": true}}, {"shard_0750": {"ok": true}}, {"shard_0751": {"ok": true}}, {"shard_0752": {"ok": true}}, {"shard_0753": {"ok": true}}, {"shard_0754": {"ok": true}}, {"shard_0755": {"ok": true}}, {"shard_0756": {"ok": true}}, {"shard_0757": {"ok": true}}, {"shard_0758": {"ok": true}}, {"shard_0759": {"ok": true}}, {"shard_0760": {"ok": true}}, {"shard_0761": {"ok": true}}, {"shard_0762": {"ok": true}}, {"shard_0763": {"ok": true}}, {"shard_0764": {"ok": true}}, {"shard_0765": {"ok": true}}, {"shard_0766": {"ok": true}}, {"shard_0767": {"ok": true}}, {"shard_0768": {"ok": true}}, {"shard_0769": {"ok": true}}, {"shard_0770": {"ok": true}}, {"shard_0771": {"ok": true}}, {"shard_0772": {"ok": true}}, {"shard_0773": {"ok": true}}, {"shard_0774": {"ok": true}}, {"shard_0775": {"ok": true}}, {"shard_0776": {"ok": true}}, {"shard_0777": {"ok": true}}, {"shard_0778": {"ok": true}}, {"shard_0779": {"ok": true}}, {"shard_0780": {"ok": true}}, {"shard_0781": {"ok": true}}, {"shard_0782": {"ok": true}}, {"shard_0783": {"ok": true}}, {"shard_0784": {"ok": true}}, {"shard_0785": {"ok": true}}, {"shard_0786": {"ok": true}}, {"shard_0787": {"ok": true}}, {"shard_0788": {"ok": true}}, {"shard_0789": {"ok": true}}, {"shard_0790": {"ok": true}}, {"shard_0791": {"ok": true}}, {"shard_0792": {"ok": true}}, {"shard_0793": {"ok": true}}, {"shard_0794": {"ok": true}}, {"shard_0795": {"ok": true}}, {"shard_0796": {"ok": true}}, {"shard_0797": {"ok": true}}, {"shard_0798": {"ok": true}}, {"shard_0799": {"ok": true}}, {"shard_0800": {"ok": true}}, {"shard_0801": {"ok": true}}, {"shard_0802": {"ok": true}}, {"shard_0803": {"ok": true}}, {"shard_0804": {"ok": true}}, {"shard_0805": {"ok": true}}, {"shard_0806": {"ok": true}}, {"shard_0807": {"ok": true}}, {"shard_0808": {"ok": true}}, {"shard_0809": {"ok": true}}, {"shard_0810": {"ok": true}}, {"shard_0811": {"ok": true}}, {"shard_0812": {"ok": true}}, {"shard_0813": {"ok": true}}, {"shard_0814": {"ok": true}}, {"shard_0815": {"ok": true}}, {"shard_0816": {"ok": true}}, {"shard_0817": {"ok": true}}, {"shard_0818": {"ok": true}}, {"shard_0819": {"ok": true}}, {"shard_0820": {"ok": true}}, {"shard_0821": {"ok": true}}, {"shard_0822": {"ok": true}}, {"shard_0823": {"ok": true}}, {"shard_0824": {"ok": true}}, {"shard_0825": {"ok": true}}, {"shard_0826": {"ok": true}}, {"shard_0827": {"ok": true}}, {"shard_0828": {"ok": true}}, {"shard_0829": {"ok": true}}, {"shard_0830": {"ok": true}}, {"shard_0831": {"ok": true}}, {"shard_0832": {"ok": true}}, {"shard_0833": {"ok": true}}, {"shard_0834": {"ok": true}}, {"shard_0835": {"ok": true}}, {"shard_0836": {"ok": true}}, {"shard_0837": {"ok": true}}, {"shard_0838": {"ok": true}}, {"shard_0839": {"ok": true}}, {"shard_0840": {"ok": true}}, {"shard_0841": {"ok": true}}, {"shard_0842": {"ok": true}}, {"shard_0843": {"ok": true}}, {"shard_0844": {"ok": true}}, {"shard_0845": {"ok": true}}, {"shard_0846": {"ok": true}}, {"shard_0847": {"ok": true}}, {"shard_0848": {"ok": true}}, {"shard_0849": {"ok": true}}, {"shard_0850": {"ok": true}}, {"shard_0851": {"ok": true}}, {"shard_0852": {"ok": true}}, {"shard_0853": {"ok": true}}, {"shard_0854": {"ok": true}}, {"shard_0855": {"ok": true}}, {"shard_0856": {"ok": true}}, {"shard_0857": {"ok": true}}, {"shard_0858": {"ok": true}}, {"shard_0859": {"ok": true}}, {"shard_0860": {"ok": true}}, {"shard_0861": {"ok": true}}, {"shard_0862": {"ok": true}}, {"shard_0863": {"ok": true}}, {"shard_0864": {"ok": true}}, {"shard_0865": {"ok": true}}, {"shard_0866": {"ok": true}}, {"shard_0867": {"ok": true}}, {"shard_0868": {"ok": true}}, {"shard_0869": {"ok": true}}, {"shard_0870": {"ok": true}}, {"shard_0871": {"ok": true}}, {"shard_0872": {"ok": true}}, {"shard_0873": {"ok": true}}, {"shard_0874": {"ok": true}}, {"shard_0875": {"ok": true}}, {"shard_0876": {"ok": true}}, {"shard_0877": {"ok": true}}, {"shard_0878": {"ok": true}}, {"shard_0879": {"ok": true}}, {"shard_0880": {"ok": true}}, {"shard_0881": {"ok": true}}, {"shard_0882": {"ok": true}}, {"shard_0883": {"ok": true}}, {"shard_0884": {"ok": true}}, {"shard_0885": {"ok": true}}, {"shard_0886": {"ok": true}}, {"shard_0887": {"ok": true}}, {"shard_0888": {"ok": true}}, {"shard_0889": {"ok": true}}, {"shard_0890": {"ok": true}}, {"shard_0891": {"ok": true}}, {"shard_0892": {"ok": true}}, {"shard_0893": {"ok": true}}, {"shard_0894": {"ok": true}}, {"shard_0895": {"ok": true}}, {"shard_0896": {"ok": true}}, {"shard_0897": {"ok": true}}, {"shard_0898": {"ok": true}}, {"shard_0899": {"ok": true}}, {"shard_0900": {"ok": true}}, {"shard_0901": {"ok": true}}, {"shard_0902": {"ok": true}}, {"shard_0903": {"ok": true}}, {"shard_0904": {"ok": true}}, {"shard_0905": {"ok": true}}, {"shard_0906": {"ok": true}}, {"shard_0907": {"ok": true}}, {"shard_0908": {"ok": true}}, {"shard_0909": {"ok": true}}, {"shard_0910": {"ok": true}}, {"shard_0911": {"ok": true}}, {"shard_0912": {"ok": true}}, {"shard_0913": {"ok": true}}, {"shard_0914": {"ok": true}}, {"shard_0915": {"ok": true}}, {"shard_0916": {"ok": true}}, {"shard_0917": {"ok": true}}, {"shard_0918": {"ok": true}}, {"shard_0919": {"ok": true}}, {"shard_0920": {"ok": true}}, {"shard_0921": {"ok": true}}, {"shard_0922": {"ok": true}}, {"shard_0923": {"ok": true}}, {"shard_0924": {"ok": true}}, {"shard_0925": {"ok": true}}, {"shard_0926": {"ok": true}}, {"shard_0927": {"ok": true}}, {"shard_0928": {"ok": true}}, {"shard_0929": {"ok": true}}, {"shard_0930": {"ok": true}}, {"shard_0931": {"ok": true}}, {"shard_0932": {"ok": true}}, {"shard_0933": {"ok": true}}, {"shard_0934": {"ok": true}}, {"shard_0935": {"ok": true}}, {"shard_0936": {"ok": true}}, {"shard_0937": {"ok": true}}, {"shard_0938": {"ok": true}}, {"shard_0939": {"ok": true}}, {"shard_0940": {"ok": true}}, {"shard_0941": {"ok": true}}, {"shard_0942": {"ok": true}}, {"shard_0943": {"ok": true}}, {"shard_0944": {"ok": true}}, {"shard_0945": {"ok": true}}, {"shard_0946": {"ok": true}}, {"shard_0947": {"ok": true}}, {"shard_0948": {"ok": true}}, {"shard_0949": {"ok": true}}, {"shard_0950": {"ok": true}}, {"shard_0951": {"ok": true}}, {"shard_0952": {"ok": true}}, {"shard_0953": {"ok": true}}, {"shard_0954": {"ok": true}}, {"shard_0955": {"ok": true}}, {"shard_0956": {"ok": true}}, {"shard_0957": {"ok": true}}, {"shard_0958": {"ok": true}}, {"shard_0959": {"ok": true}}, {"shard_0960": {"ok": true}}, {"shard_0961": {"ok": true}}, {"shard_0962": {"ok": true}}, {"shard_0963": {"ok": true}}, {"shard_0964": {"ok": true}}, {"shard_0965": {"ok": true}}, {"shard_0966": {"ok": true}}, {"shard_0967": {"ok": true}}, {"shard_0968": {"ok": true}}, {"shard_0969": {"ok": true}}, {"shard_0970": {"ok": true}}, {"shard_0971": {"ok": true}}, {"shard_0972": {"ok": true}}, {"shard_0973": {"ok": true}}, {"shard_0974": {"ok": true}}, {"shard_0975": {"ok": true}}, {"shard_0976": {"ok": true}}, {"shard_0977": {"ok": true}}, {"shard_0978": {"ok": true}}, {"shard_0979": {"ok": true}}, {"shard_0980": {"ok": true}}, {"shard_0981": {"ok": true}}, {"shard_0982": {"ok": true}}, {"shard_0983": {"ok": true}}, {"shard_0984": {"ok": true}}, {"shard_0985": {"ok": true}}, {"shard_0986": {"ok": true}}, {"shard_0987": {"ok": true}}, {"shard_0988": {"ok": true}}, {"shard_0989": {"ok": true}}, {"shard_0990": {"ok": true}}, {"shard_0991": {"ok": true}}, {"shard_0992": {"ok": true}}, {"shard_0993": {"ok": true}}, {"shard_0994": {"ok": true}}, {"shard_0995": {"ok": true}}, {"shard_0996": {"ok": true}}, {"shard_0997": {"ok": true}}, {"shard_0998": {"ok": true}}, {"shard_0999": {"ok": true}}, {"shard_1000": {"ok": true}}, {"shard_1001": {"ok": true}}, {"shard_1002": {"ok": true}}, {"shard_1003": {"ok": true}}, {"shard_1004": {"ok": true}}, {"shard_1005": {"ok": true}}, {"shard_1006": {"ok": true}}, {"shard_1007": {"ok": true}}, {"shard_1008": {"ok": true}}, {"shard_1009": {"ok": true}}, {"shard_1010": {"ok": true}}, {"shard_1011": {"ok": true}}, {"shard_1012": {"ok": true}}, {"shard_1013": {"ok": true}}, {"shard_1014": {"ok": true}}, {"shard_1015": {"ok": true}}, {"shard_1016": {"ok": true}}, {"shard_1017": {"ok": true}}, {"shard_1018": {"ok": true}}, {"shard_1019": {"ok": true}}, {"shard_1020": {"ok": true}}, {"shard_1021": {"ok": true}}, {"shard_1022": {"ok": true}}, {"shard_1023": {"ok": true}}, {"shard_1024": {"ok": true}}]}

Possible resolutions

  • Provide a way to specify the set of databases to check
  • Provide a way to specify a percentage of databases to check
  • Connection pooling on the application level

Add header support for token authentication

This will be both cleaner, and more secure (not logging authentication token in access logs, etc).

Determine if there's a "standard" authentication header, or if we should just use something like X-Authentication-Token or similar.

Sort caches / dbs

For apps with large numbers of dbs or caches, random sorting can make it hard to find a specific one - we should instead sort by the first key in the dictionary:

{
    ...
    "databases": [
        {
            "transactions_shard_004": {
                "ok": true
            }
        },
        {
            "transactions_shard_010": {
                "ok": true
            }
        },
        {
            "bookkeeping_shard_001": {
                "ok": true
            }
        },
        {
            "transactions_shard_005": {
                "ok": true
            }
        },
        {
            "bookkeeping_shard_010": {
                "ok": true
            }
        },
        {
            "bookkeeping_shard_002": {
                "ok": true
            }
        },
        {
            "bookkeeping_shard_005": {
                "ok": true
            }
        },
        {
            "bookkeeping_shard_004": {
                "ok": true
            }
        },
        {
            "bookkeeping_shard_007": {
                "ok": true
            }
        },
        {
            "bookkeeping_shard_006": {
                "ok": true
            }
        },
        {
            "bookkeeping_shard_009": {
                "ok": true
            }
        },
        {
            "bookkeeping_shard_008": {
                "ok": true
            }
        },
        {
            "transactions_shard_007": {
                "ok": true
            }
        },
        {
            "transactions_shard_009": {
                "ok": true
            }
        },
        {
            "transactions_shard_003": {
                "ok": true
            }
        },
        {
            "transactions_shard_006": {
                "ok": true
            }
        },
        {
            "default": {
                "ok": true
            }
        },
        {
            "income_expense_shard_010": {
                "ok": true
            }
        },
        {
            "bookkeeping_shard_003": {
                "ok": true
            }
        },
        {
            "transactions_shard_008": {
                "ok": true
            }
        },
        {
            "income_expense_shard_008": {
                "ok": true
            }
        },
        {
            "income_expense_shard_009": {
                "ok": true
            }
        },
        {
            "transactions_shard_002": {
                "ok": true
            }
        },
        {
            "income_expense_shard_004": {
                "ok": true
            }
        },
        {
            "income_expense_shard_005": {
                "ok": true
            }
        },
        {
            "income_expense_shard_006": {
                "ok": true
            }
        },
        {
            "income_expense_shard_007": {
                "ok": true
            }
        },
        {
            "transactions_shard_001": {
                "ok": true
            }
        },
        {
            "income_expense_shard_001": {
                "ok": true
            }
        },
        {
            "income_expense_shard_002": {
                "ok": true
            }
        },
        {
            "income_expense_shard_003": {
                "ok": true
            }
        }
    ]
}

drop empty static files

Everything in watchman/static/ are empty files and have been since they were added by cookie-cutter... I think it's safe to just remove them.

I noticed when I ran manage.py collectstatic and js/watchman.js popped up in my static directory.

Online demo / easy set up of example site

  • Deploy a sample app to Heroku
  • Provide a Vagrantfile
  • Provide an example Heroku app

app.json

{
  "name": "django-watchman demo",
  "description": "Sets up a demo django app with django-watchman installed.",
  "keywords": [
    "django",
    "app",
    "monitoring"
  ],
  "website": "https://github.com/mwarkentin/django-watchman",
  "repository": "https://github.com/mwarkentin/django-watchman",
  "success_url": "/watchman",
  "env": {
    "SECRET_KEY": {
      "description": "A secret key for verifying the integrity of signed cookies.",
      "generator": "secret"
    }
  },
  "addons": [
    "heroku-postgresql",
    "openredis"
  ],
  "buildpacks": [
    {
      "url": "https://github.com/heroku/heroku-buildpack-python"
    }
  ]
}

Check rabbitmq

See https://github.com/waveaccounting/payments/blob/master/payments/utils/checks.py#L9-L32

# -*- coding: utf-8 -*-

import traceback

from django.conf import settings
import pika


def rabbitmq_connection_check():
    # These are checks for Django Watchman
    def _connect():
        try:
            creds = pika.PlainCredentials(settings.PIKA_CONNECTION_PARAMS['username'], settings.PIKA_CONNECTION_PARAMS['password'])
            conn_params = pika.ConnectionParameters(
                settings.PIKA_CONNECTION_PARAMS['hostname'],
                settings.PIKA_CONNECTION_PARAMS['port'],
                settings.PIKA_CONNECTION_PARAMS['vhost'],
                creds,
            )
            connection = pika.BlockingConnection(conn_params)
            connection.close()
            return {'ok': True}
        except Exception as e:
            return {
                'ok': False,
                'error': unicode(e),
                'stacktrace': traceback.format_exc(),
            }

    return {
        'rabbitmq': _connect()
    }

Tests are failing after django 1.7 release

Failing build

nosetests tests -s --verbosity=1
Creating test database for alias 'default'...
Traceback (most recent call last):
  File "runtests.py", line 44, in <module>
    run_tests(*sys.argv[1:])
  File "runtests.py", line 37, in run_tests
    failures = test_runner.run_tests(test_args)
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/django_nose/runner.py", line 200, in run_tests
    result = self.run_suite(nose_argv)
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/django_nose/runner.py", line 147, in run_suite
    addplugins=plugins_to_add)
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/nose/core.py", line 121, in __init__
    **extra_args)
  File "/opt/python/2.7.8/lib/python2.7/unittest/main.py", line 95, in __init__
    self.runTests()
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/nose/core.py", line 207, in runTests
    result = self.testRunner.run(self.test)
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/nose/core.py", line 50, in run
    wrapper = self.config.plugins.prepareTest(test)
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/nose/plugins/manager.py", line 99, in __call__
    return self.call(*arg, **kw)
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/nose/plugins/manager.py", line 167, in simple
    result = meth(*arg, **kw)
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/django_nose/plugin.py", line 75, in prepareTest
    self.old_names = self.runner.setup_databases()
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/django_nose/runner.py", line 383, in setup_databases
    return super(NoseTestSuiteRunner, self).setup_databases()
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/django/test/runner.py", line 109, in setup_databases
    return setup_databases(self.verbosity, self.interactive, **kwargs)
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/django/test/runner.py", line 299, in setup_databases
    serialize=connection.settings_dict.get("TEST_SERIALIZE", True),
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/django/db/backends/creation.py", line 374, in create_test_db
    test_flush=True,
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/django/core/management/__init__.py", line 93, in call_command
    app_name = get_commands()[name]
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/django/utils/lru_cache.py", line 101, in wrapper
    result = user_function(*args, **kwds)
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/django/core/management/__init__.py", line 73, in get_commands
    for app_config in reversed(list(apps.get_app_configs())):
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/django/apps/registry.py", line 137, in get_app_configs
    self.check_apps_ready()
  File "/home/travis/virtualenv/python2.7.8/lib/python2.7/site-packages/django/apps/registry.py", line 124, in check_apps_ready
    raise AppRegistryNotReady("Apps aren't loaded yet.")
AppRegistryNotReady: Apps aren't loaded yet.
The command "coverage run --source watchman runtests.py" exited with 1.
Done. Your build exited with 1.

local variable 'type_overall_status' referenced before assignment

I'm using 0.9.0 but this seems to be an issue in the repo too. I'm just using this project for the first time, so I suspect I have something else wrong, but there's a variable that's assigned with in an if elif which makes it possible to be undefined. The line that throws the exception is: https://github.com/mwarkentin/django-watchman/blob/master/watchman/views.py#L134

From the comments and a presentation I watched, it seems that the stacktrace should be a string, but the code seems to be checking for either a dict or a list.

Status dashboard

Human-readable grid that shows your various backing services with green/red status indicators.

Check for required environment variables

Configure a list of environment variables to assert are set in settings.py.

Should there be some default environment variables to check?

  • SECRET_KEY
  • WATCHMAN_TOKENS
  • ??

Refactor python 2/3 test compatibility

There are a few ways of dealing with checking that items are equal:

test_utils.py:

def assertListsEqual(self, l1, l2):
    try:
        # Python 3.4
        self.assertCountEqual(l1, l2)
    except AttributeError:
        # Python 2.7
        self.assertItemsEqual(l1, l2)

test_views.py

PYTHON_VERSION = sys.version_info[0]

if PYTHON_VERSION == 2:
        content = json.loads(response.content)
        self.assertItemsEqual(expected_checks, content.keys())
    else:
        content = json.loads(response.content.decode('utf-8'))
        self.assertCountEqual(expected_checks, content.keys())

Support multiple auth tokens

Use cases:

  • "Non downtime" rotations: add a second token temporarily, update all monitoring to use second token, remove first token
  • Different tokens for different users: could enable improved access logging, reduce impact of a token leak, etc.

Current design

Set WATCHMAN_TOKEN to a single token value

Proposed design

New environment variable: WATCHMAN_TOKENS

This would be a comma-separated (TBD) list of tokens which are all valid.

error on sphinx using mysql interface

I upgraded to Django 1.8 and am now getting the following exception on one of my databases:

Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/watchman/decorators.py", line 17, in wrapped
    response = func(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/watchman/checks.py", line 39, in _check_database
    connections[database].introspection.table_names()
  File "/usr/local/lib/python2.7/site-packages/django/db/backends/base/introspection.py", line 57, in table_names
    return get_names(cursor)
  File "/usr/local/lib/python2.7/site-packages/django/db/backends/base/introspection.py", line 53, in get_names
    return sorted(ti.name for ti in self.get_table_list(cursor)
  File "/usr/local/lib/python2.7/site-packages/django/db/backends/mysql/introspection.py", line 48, in get_table_list
    cursor.execute("SHOW FULL TABLES")
  File "/usr/local/lib/python2.7/site-packages/django/db/backends/utils.py", line 79, in execute
    return super(CursorDebugWrapper, self).execute(sql, params)
  File "/usr/local/lib/python2.7/site-packages/django/db/backends/utils.py", line 64, in execute
    return self.cursor.execute(sql, params)
  File "/usr/local/lib/python2.7/site-packages/django/db/utils.py", line 98, in __exit__
    six.reraise(dj_exc_type, dj_exc_value, traceback)
  File "/usr/local/lib/python2.7/site-packages/django/db/backends/utils.py", line 62, in execute
    return self.cursor.execute(sql)
  File "/usr/local/lib/python2.7/site-packages/django/db/backends/mysql/base.py", line 124, in execute
    return self.cursor.execute(query, args)
  File "/usr/local/lib/python2.7/site-packages/MySQLdb/cursors.py", line 205, in execute
    self.errorhandler(self, exc, value)
  File "/usr/local/lib/python2.7/site-packages/MySQLdb/connections.py", line 36, in defaulterrorhandler
    raise errorclass, errorvalue
ProgrammingError: (1064, "sphinxql: syntax error, unexpected IDENT, expecting VARIABLES near 'FULL TABLES'")

The database is sphinx using the MySQL interface. So, it behaves a little different than a regular MySQL db, but has a listing in the DATABASES settings like the rest.

I'm guessing I'll have to exclude this database connection using WATCHMAN_DATABASES and then write my own test. Do you have any suggestions for testing the database in an alternative fashion?

I'll see what I can figure out and report back here... I'm really just reporting the issue in case anyone else has a similar problem or if there's an existing solution. The default test did work fine in Django 1.7 so I'm not sure what changed.

Customize HTTP status code on failed checks

The biggest problem we hit dealt with the inability to change the HTTP status codes on specific failed checks.

For example, when some checks fail, we also want the status code returned by django-watchman to be a non HTTP 200. That allows something like Amazon's Elastic Load Balancer to mark the failing node for replacement.

Default to `500` response code when there's an error

Related: #43, #46

I've come across a few use cases recently which are better handled by returning a 5XX error code when there's an issue rather than using the contents of the response to determine.

We should update the default error code in the next release to be 500.

Build hubot-django-watchman

hubot plugin for checking on watchman status from within slack, hipchat, etc.

  • store list of watchman urls as comma-separated environment variable (`HUBOT_DJANGO_WATCHMAN_URLS)
  • check all available watchman urls: @hubot watchman me
  • check specific watchman url: @hubot watchman me <url>

Questions

  • How to display response?

Information leakage: X-Watchman-Version

As raised by @blag in #35:

if an attacker can get the version of watchman, they can limit their known range of versions of Django (eg: version x.y.z of watchman only supports version a.b.c through d.e.f of Django). Given this information, an attacker can refine their attack strategy, allowing them to attempt fewer exploits than they would otherwise. That, in turn, makes it easier to evade IDS systems.

Basically: version information disclosure to malicious parties violates defense-in-depth guidelines.

Now, this is only an issue when either of the following is true:

there is no authentication done on the status or dashboard endpoints
a malicious party has access to the status or dashboard endpoints
The first is unlikely yet possible with PR #30 if the authentication/authorization decorator is flawed, and the second is a misplaced trust issue to being with.

Given all of that, this isn't a huge deal, I just want to recognize that this PR creates a small, mild defense-in-depth violation for essentially what are misconfigured systems already. Making a note about this issue in the documentation for watchman might be a good idea to help users avoid this potential pitfall. But I think "secure by default" should be the accepted practice, so I would also suggest turning off this feature by default, and letting users turn it on if they wish.

Add `ENABLE_PAID_CHECKS` setting or similar

Rather than having to modify the WATCHMAN_CHECKS setting in order to enable the email check (and potentially others which may need to be disabled by default due to potential cost) - instead there could be a single toggle which enables / disables any paid checks.

Default to False.

Add setting to disable/log stacktrace

I would like an option to suppress stacktraces from being returned by the watchman views.

Add the following new settings:

WATCHMAN_RENDER_STACKTRACE = (True|False) - If enabled, watchman will return the stacktrace in the json and dashboard views.

WATCHMAN_LOG_STACKTRACE = (True|False) - If enabled, watchman will log stacktraces to the django logger.

Check Redis

This is for redis usage outside of the built-in django caching functionality, which we already have a check for.

  • Will require a WATCHMAN_REDIS_URL(S) setting
  • Is there a "standard" redis setting which we could try first?
    • REDIS_URL
    • DJANGO_REDIS_URL
  • How do we know which redis lib is installed? Do we need to try to support multiples?
  • Same checks as our cache check:
    • set key
    • get key
    • delete key

Docs for new comers: Calling side of django-watchman

I think it would help new comers, if you could tell some words about the "calling side" of django-watchman. I mean the client which calls the http end point and which collects and acts on the reported status messages.

I know that this is not the job of django-watchman, but it helps new comers to understand the big picture.

watchman.checks.storage should write bytes to test-file on Py3

Hi, i'm giving django-minio-storage a try and got an exception when storage-check is executed. I'm running Python 3.4.5 and Django 1.10.6.

Traceback (most recent call last):
  File "/Users/daniel/Workspace/my_project/lib/python3.4/site-packages/watchman/decorators.py", line 17, in wrapped
    response = func(*args, **kwargs)
  File "/Users/daniel/Workspace/my_project/lib/python3.4/site-packages/watchman/checks.py", line 63, in _check_storage
    path = default_storage.save(filename, ContentFile(content))
  File "/Users/daniel/Workspace/my_project/lib/python3.4/site-packages/django/core/files/storage.py", line 54, in save
    return self._save(name, content)
  File "/Users/daniel/Workspace/my_project/lib/python3.4/site-packages/minio_storage/storage.py", line 77, in _save
    content_type)
  File "/Users/daniel/Workspace/my_project/lib/python3.4/site-packages/minio/api.py", line 894, in put_object
    current_data, metadata=metadata)
  File "/Users/daniel/Workspace/my_project/lib/python3.4/site-packages/minio/api.py", line 1557, in _do_put_object
    raise ValueError('Input data must be bytes type')
ValueError: Input data must be bytes type

I was able to fix it by using a byte instead of str writing to the dummy file. This should basically work on Python 2.7 - i'd file a PR later.

setup.py should not import main package

The setup script should not import the main package.

watchman/__init__.py may grow in the future. For example, it might be useful to raise certain objects to the top level of the API for a nicer user experience:

from watchman.deeply.nested import something_useful

At present, this would lead to an ImportError on installation, since setuptools would try to import watchman.deeply.nested before it is actually installed.

Here are some proposed solutions: https://stackoverflow.com/q/17583443/. I'm opening this as an issue, rather than a PR, since I think this is a decision for the lead developer.

I'm a fan of bumpversion, since it would tag new releases automatically too.

Here's an example .bumpversion.cfg:

[bumpversion]
current_version = 0.8.0
commit = True
tag = True

[bumpversion:file:watchman/__init__.py]
[bumpversion:file:setup.py]

Clean up module level settings

Brought up by @kezabelle in #12:

The side-effect of doing it this way (and I'm guilty of it myself; a lot) is that it is cached at the module-level. It mostly isn't a problem for the app as-is because the biggest use-case it may break is override_settings, which isn't being used.

There was a ticket about documenting a specific way, but no resolution came of it.

Django 1.10: AttributeError: type object 'BaseCommand' has no attribute 'option_list'

Looks like there's a breaking change in Django 1.10: https://travis-ci.org/mwarkentin/django-watchman/jobs/131705036

$ coverage run --source watchman runtests.py
Coverage.py warning: No data was collected.
Traceback (most recent call last):
  File "runtests.py", line 25, in <module>
    from django_nose import NoseTestSuiteRunner
  File "/home/travis/virtualenv/python2.7.9/lib/python2.7/site-packages/django_nose/__init__.py", line 5, in <module>
    from django_nose.runner import BasicNoseRunner, NoseTestSuiteRunner
  File "/home/travis/virtualenv/python2.7.9/lib/python2.7/site-packages/django_nose/runner.py", line 285, in <module>
    class BaseRunner(DiscoverRunner):
  File "/home/travis/virtualenv/python2.7.9/lib/python2.7/site-packages/django_nose/runner.py", line 287, in BaseRunner
    options = _get_options()
  File "/home/travis/virtualenv/python2.7.9/lib/python2.7/site-packages/django_nose/runner.py", line 143, in _get_options
    django_opts = [opt.dest for opt in BaseCommand.option_list] + ['version']
AttributeError: type object 'BaseCommand' has no attribute 'option_list'
The command "coverage run --source watchman runtests.py" exited with 1.

It was deprecated in 1.8: https://docs.djangoproject.com/en/1.8/howto/custom-management-commands/#django.core.management.BaseCommand.option_list

New example on how to do optional arguments: https://docs.djangoproject.com/en/1.8/howto/custom-management-commands/#custom-commands-options

Example fix for django-waffle: ldng/django-waffle@700bb99

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.