0

Yet another one of these!

What i'm running:
Ubuntu 18.04.3 LTS
Python 3.6.9
Bottle 0.12.18
Grafana v6.5.1
SimpleJson 1.4.0
Chrome 78.0.3904.108

I'm trying to get Grafana to talk to my Python Bottle server with the end goal of pulling data from a MongoDB for that nice visualization.
My attempt is based on this example. It's not very explicit in it's descriptions but it's the best i found and after som tweaking i got it running without any immediate errors. I also modified the CORS enable part according to this answer by user "ron rothman". It currently looks like this:

#!/usr/bin/env python3
import math

from datetime import datetime
from calendar import timegm
from bottle import Bottle, HTTPResponse, run, request, response, json_dumps as dumps

class EnableCors(object):
    name = 'enable_cors'
    api = 2

    def apply(self, fn, context):
        def _enable_cors(*args, **kwargs):
            # set CORS headers
            for element in response:
                print(element)

            response.headers['Access-Control-Allow-Origin'] = "*"
            response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
            response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'

            if request.method != 'OPTIONS':
                # actual request; reply with the actual response
                return fn(*args, **kwargs)

        return _enable_cors

FUNCTIONS = {'series A': math.sin, 'series B': math.cos}

app = Bottle()


def convert_to_time_ms(timestamp):
    return 1000 * timegm(
            datetime.strptime(
                timestamp, '%Y-%m-%dT%H:%M:%S.%fZ').timetuple())


def create_data_points(function, start, end, length=1020):
    lower = convert_to_time_ms(start)
    upper = convert_to_time_ms(end)
    return [[function(i), int(i)] for i in [
        lower + x*(upper-lower)/length for x in range(length)]]



@app.route("/", method='GET')
@app.route("/", method='OPTIONS')
def index():
    return "OK"


@app.post('/search', method='OPTIONS')
@app.post('/search', method='POST')
@app.post('/search', method='GET')
def search():
    return HTTPResponse(body=dumps(['series A', 'series B']),
                        headers={'Content-Type': 'application/json'})


@app.post('/query', method='OPTIONS')
@app.post('/query', method='POST')
@app.post('/query', method='GET')
def query():
    print(request.json)
    if request.json['targets'][0]['type'] == 'table':
        series = request.json['targets'][0]['target']
        bodies = {'series A': [{
            "columns": [
                {"text": "Time", "type": "time"},
                {"text": "Country", " type": "string"},
                {"text": "Number", "type": "number"}
            ],
            "rows": [
                [1234567, "SE", 123],
                [1234567, "DE", 231],
                [1234567, "US", 321]
            ],
            "type": "table"
            }],
            'series B': [{"columns": [
                {"text": "Time", "type": "time"},
                {"text": "Country", "type": "string"},
                {"text": "Number", "type": "number"}
                ],
                "rows": [
                [1234567, "BE", 123],
                [1234567, "GE", 231],
                [1234567, "PS", 321]
            ],
                "type": "table"
            }]}

        series = request.json['targets'][0]['target']
        body = dumps(bodies[series])
    else:
        body = []

        body = dumps(body)

    return HTTPResponse(body=body,
                        headers={'Content-Type': 'application/json'})

app.install(EnableCors())

app.run(port = 8081)

In the settings Grafana accepts the server, "Data source is working". How ever as soon as i try do add a dashboard panel i get three errors.

new?panelId=2&edit&fullscreen&orgId=1:1 Access to XMLHttpRequest at 'http://localhost:8081/search' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

,

Possibly unhandled rejection: {"err":{"data":null,"status":-1,"config":{"method":"POST","transformRequest":[null],"transformResponse":[null],"jsonpCallbackParam":"callback","url":"http://localhost:8081/search","data":{"target":""},"headers":{"Content-Type":"application/json","Accept":"application/json, text/plain, */*"},"retry":0},"statusText":"","xhrStatus":"error"},"cancelled":true}

and

new?panelId=2&edit&fullscreen&orgId=1:1 Access to XMLHttpRequest at 'http://localhost:8081/query' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

bottle responds with

Bottle v0.12.18 server starting up (using WSGIRefServer())...
Listening on http://127.0.0.1:8081/
Hit Ctrl-C to quit.

127.0.0.1 - - [05/Dec/2019 15:41:35] "OPTIONS /search HTTP/1.1" 200 0
127.0.0.1 - - [05/Dec/2019 15:41:35] "POST /search HTTP/1.1" 200 24
127.0.0.1 - - [05/Dec/2019 15:41:35] "OPTIONS /query HTTP/1.1" 200 0
{'requestId': 'Q100', 'timezone': '', 'panelId': 2, 'dashboardId': None, 'range': {'from': '2019-12-05T08:41:35.236Z', 'to': '2019-12-05T14:41:35.237Z', 'raw': {'from': 'now-6h', 'to': 'now'}}, 'interval': '1m', 'intervalMs': 60000, 'targets': [{'refId': 'A', 'type': 'timeserie'}], 'maxDataPoints': 335, 'scopedVars': {'__interval': {'text': '1m', 'value': '1m'}, '__interval_ms': {'text': '60000', 'value': 60000}}, 'startTime': 1575556895466, 'rangeRaw': {'from': 'now-6h', 'to': 'now'}, 'adhocFilters': []}
127.0.0.1 - - [05/Dec/2019 15:41:35] "POST /query HTTP/1.1" 200 2

I think i've verified that the response.headers['Access-Control-Allow-Origin'] = "*" actually does something by setting it to 'foo' and then getting

The 'Access-Control-Allow-Origin' header contains the invalid value 'foo'.

instead.

Soon i've spent an entire workday on this and would really appreciate some help from you, thanks!

Enok82
  • 163
  • 1
  • 13

1 Answers1

1

The wildcard response.headers['Access-Control-Allow-Origin'] = "*" often doesn't work if you have credentials or something See here. Its best to explicitly take the origin and add it in your response header

What has often worked is to add an after_request in the flask

@app.after_request()
def add_headers(response): 
   response.headers['Access-Control-Allow-Origin'] =  request.environ.get('HTTP_ORIGIN')
   return response

you can similarly add any other headers as well

Vipluv
  • 884
  • 7
  • 23
  • is ```@app.hook('after_request')``` equivalent in bottle to ```@app.after_request()``` in flask? – Enok82 Dec 05 '19 at 15:44
  • Also is there any reason why i shouldn't be able to do the ```request.environ.get('HTTP_ORIGIN')``` where i'm already setting it to *? – Enok82 Dec 05 '19 at 15:52
  • it seems the bottle command is equivalent as per https://stackoverflow.com/questions/26849770/status-code-of-response-is-incorrect-in-after-request-hook-in-a-bottle-app but the origin header should be in your request - and it shouldnt be * but the actual url. And yes -- you should use get see this one - https://stackoverflow.com/questions/28448991/flask-how-to-get-the-http-origin-of-a-request – Vipluv Dec 05 '19 at 16:01
  • I tried this while using Bottle to no avail. This made me swap to Flask. And using Flask with ```request.environ.get('HTTP_ORIGIN')``` is working. I'll post the complete version in a separate answer. Is it possible that there's a bug in Bottle? – Enok82 Dec 06 '19 at 14:44