Search code examples
pythonflaskdatadogflasgger

Combination of @statsd.timed and @swag_from decorators causes FileNotFoundError


I have a working Flask endpoint decorated with flasgger.swag_from decorator:

from flasgger import swag_from

@app.route("/jobs", methods=["GET"])
@swag_from('swagger/get_jobs.yml')
def get_jobs(request_id):
   # ...

This works as expected.

I'm using DataDog and would like to time every call to this endpoint. Based on this documentation, this can easily be achieved by adding the timed decorator:

from flasgger import swag_from
from datadog import statsd

@app.route("/jobs", methods=["GET"])
@swag_from('swagger/get_jobs.yml')
@statsd.timed("api_call.duration")
def get_jobs(request_id):
   # ...

However - once I do that, I get an error when trying to render the Swagger page:

enter image description here

The log shows the error is a FileNotFoundError caused by Swagger looking for the yml file in the wrong location:

FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/lib/python3.7/site-packages/datadog/dogstatsd/swagger/backup_sources.yml'

Based on this post, this error sometimes happens when Flask's "static_folder" is configured incorrectly. I've tried changing it, using Flask(__name__, static_folder='./'), but the same issue persists.


Solution

  • Found the issue.

    Turns out that if you provide a relative path to @swag_from (as I do above: swagger/get_jobs.yml), Flassger "guesses" the root path by calling:

    filename = os.path.abspath(obj.__globals__['__file__'])
    return os.path.dirname(filename)
    

    When I added the @statsd.timed decorator below the @swag_from decorator, I'm basically running the statsd decorator "before" the Flassger one - i.e. the Flassger decorator runs over a wrapper function generated by statsd, which means the obj.__globals__['__file__'] is the statsd source file, which will make Flassger look for the swagger file under /usr/local/lib/python3.7/site-packages/datadog/dogstatsd.

    The simple workaround is to reverse the decorator order, i.e. running the Flasgger decorator "before" the statsd one, by placing it second:

    @app.route("/jobs", methods=["GET"])
    @statsd.timed("api_call.duration")   # must be above swag_from
    @swag_from('swagger/get_jobs.yml')   # must be below statsd.timed
    def get_jobs(request_id):
       # ...
    

    There might be other solutions - providing an absolute path in swag_from or somehow setting a root_path attribute on the get_jobs method (not sure how to do that).