blob: 99ad62f6cf543171ea01119d85e46edb0f769402 [file] [log] [blame]
# Copyright 2022 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
try:
import html
except ImportError: # pragma: no cover
import cgi as html
import json
import logging
import time
from infra_libs.ts_mon.common import distribution
from infra_libs.ts_mon.common import interface
from infra_libs.ts_mon.common import metrics
class TSMonJSFlaskHandler(object):
"""Proxy handler for ts_mon metrics collected in JavaScript.
To use this class:
1. Subclass it and override self.xsrf_is_valid
2. After instantiation call self.register_metrics to register global metrics.
"""
def __init__(self, flask, services=None):
self.flask = flask
self._metrics = None
self.response = None
self.request = self.flask.request
self.service = services
def register_metrics(self, metrics_list):
"""Registers ts_mon metrics, required for use.
Args:
metrics_list: a list of definitions, from ts_mon.metrics.
"""
interface.register_global_metrics(metrics_list)
self._metrics = self._metrics or {}
for metric in metrics_list:
if metric.is_cumulative():
metric.dangerously_enable_cumulative_set()
self._metrics[metric.name] = metric
def post(self):
"""POST expects a JSON body that's a dict which includes a key "metrics".
This key's value is an array of objects with schema:
{
"metrics": [{
"MetricInfo": {
"Name": "monorail/frontend/float_test",
"ValueType": 2
},
"Cells": [{
"value": 1,
"fields": {},
"start_time": 1538430628174
}]
}]
}
Important!
The user of this library is responsible for validating XSRF tokens via
implementing the method self.xsrf_is_valid.
"""
self.response = self.flask.make_response()
self.response.headers.add('Content-Security-Policy', "default-src 'none'")
self.response.content_type = 'text/plain; charset=UTF-8'
if not self._metrics:
self.response.status_code = 400
self.response.data = 'No metrics have been registered.'
logging.warning('gae_ts_mon error: No metrics have been registered.')
return self.response
try:
# TODO: (crbug.com/monorail/10992) update the JSON parse functions
body = json.loads(self.request.get_data(as_text=True))
except ValueError:
self.response.status_code = 400
self.response.data = 'Invalid JSON.'
logging.warning('gae_ts_mon error: Invalid JSON.')
return self.response
if not self.xsrf_is_valid(body):
self.response.status_code = 403
self.response.data = 'XSRF token invalid.'
logging.warning('gae_ts_mon error: XSRF token invalid.')
return self.response
if not isinstance(body, dict):
self.response.status_code = 400
self.response.data = 'Body must be a dictionary.'
logging.warning('gae_ts_mon error: Body must be a dictionary.')
return self.response
if 'metrics' not in body:
self.response.status_code = 400
self.response.data = 'Key "metrics" must be in body.'
logging.warning('gae_ts_mon error: Key "metrics" must be in body.')
logging.warning('Request body: %s', body)
return self.response
for metric_measurement in body.get('metrics', []):
name = metric_measurement.get('MetricInfo').get('Name')
metric = self._metrics.get(name, None)
if not metric:
self.response.status_code = 400
self.response.data = ('Metric "%s" is not defined.' % html.escape(name))
logging.warning('gae_ts_mon error: Metric "%s" is not defined.', name)
return self.response
for cell in metric_measurement.get('Cells', []):
fields = cell.get('fields', {})
value = cell.get('value')
metric_field_keys = set(fs.name for fs in metric.field_spec)
if set(fields.keys()) != metric_field_keys:
self.response.status_code = 400
self.response.data = ('Supplied fields do not match metric "%s".' %
html.escape(name))
logging.warning(
'gae_ts_mon error: Supplied fields do not match metric "%s".',
name)
logging.warning('Supplied fields keys: %s', list(fields.keys()))
logging.warning('Metric fields keys: %s', metric_field_keys)
return self.response
start_time = cell.get('start_time')
if metric.is_cumulative() and not start_time:
self.response.status_code = 400
self.response.data = 'Cumulative metrics must have start_time.'
logging.warning(
'gae_ts_mon error: Cumulative metrics must have start_time.')
logging.warning('Metric name: %s', name)
return self.response
if metric.is_cumulative() and not self._start_time_is_valid(start_time):
self.response.status_code = 400
self.response.data = ('Invalid start_time: %s.' %
html.escape(str(start_time)))
logging.warning('gae_ts_mon error: Invalid start_time: %s.',
start_time)
return self.response
# Convert distribution metric values into Distribution classes.
if (isinstance(metric, (metrics.CumulativeDistributionMetric,
metrics.NonCumulativeDistributionMetric))):
if not isinstance(value, dict):
self.response.status_code = 400
self.response.data = (
'Distribution metric values must be a dict.')
logging.warning(
'gae_ts_mon error: Distribution metric values must be a dict.')
logging.warning('Metric value: %s', value)
return self.response
dist_value = distribution.Distribution(bucketer=metric.bucketer)
dist_value.sum = value.get('sum', 0)
dist_value.count = value.get('count', 0)
dist_value.buckets.update(value.get('buckets', {}))
metric.set(dist_value, fields=fields)
else:
metric.set(value, fields=fields)
if metric.is_cumulative():
metric.dangerously_set_start_time(start_time)
self.response.status_code = 201
self.response.data = 'Ok.'
return self.response
def xsrf_is_valid(self, _body): # pragma: no cover
"""Takes a request body and returns whether the included XSRF token
is valid.
This method must be implemented by a subclass.
"""
raise NotImplementedError(
'xsrf_is_valid must be implemented in a subclass.')
def time_fn(self):
"""Defaults to time.time. Can be overridden for testing."""
return time.time()
def _start_time_is_valid(self, start_time):
"""Validates that a start_time is not in the future and not
more than a month in the past.
"""
now = self.time_fn()
if start_time > now:
return False
one_month_seconds = 60 * 60 * 24 * 30
one_month_ago = now - one_month_seconds
if start_time < one_month_ago:
return False
return True