-
Notifications
You must be signed in to change notification settings - Fork 159
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #268 from flask-dashboard/reporting
Reporting
- Loading branch information
Showing
22 changed files
with
737 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
class DateInterval(object): | ||
def __init__(self, start_date, end_date): | ||
if start_date > end_date: | ||
raise ValueError('start_date must be before or equals to end_date') | ||
|
||
self._start_date = start_date | ||
self._end_date = end_date | ||
|
||
def start_date(self): | ||
return self._start_date | ||
|
||
def end_date(self): | ||
return self._end_date | ||
|
||
def __repr__(self): | ||
return str((self._start_date, self._end_date)) |
Empty file.
33 changes: 33 additions & 0 deletions
33
flask_monitoringdashboard/core/reporting/mean_permutation_test.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from __future__ import division | ||
import numpy as np | ||
|
||
|
||
def mean_diff(x, y): | ||
return np.abs(np.mean(x) - np.mean(y)) | ||
|
||
|
||
def mean_permutation_test(x, y, num_rounds=1000): | ||
""" | ||
Performs a non-parametric test to check whether `x` and `y` come from the same distribution. | ||
:param x: a sample from some distribution | ||
:param y: a sample to compare x to | ||
:param num_rounds: number of different permutations to test. Increase this number to increase | ||
the accuracy | ||
:return: The p-value | ||
""" | ||
rng = np.random.RandomState() | ||
|
||
m = len(x) | ||
combined = np.hstack((x, y)) | ||
|
||
more_extreme = 0 | ||
reference_stat = mean_diff(x, y) | ||
|
||
for i in range(num_rounds): | ||
rng.shuffle(combined) | ||
|
||
if mean_diff(combined[:m], combined[m:]) > reference_stat: | ||
more_extreme += 1 | ||
|
||
return more_extreme / num_rounds |
Empty file.
71 changes: 71 additions & 0 deletions
71
flask_monitoringdashboard/core/reporting/questions/average_latency.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
from flask_monitoringdashboard.core.reporting.mean_permutation_test import mean_permutation_test | ||
import numpy as np | ||
|
||
from flask_monitoringdashboard.core.reporting.questions.report_question import Answer, ReportQuestion | ||
from flask_monitoringdashboard.database import session_scope | ||
|
||
from flask_monitoringdashboard.database.request import get_latencies_sample | ||
|
||
|
||
class AverageLatencyAnswer(Answer): | ||
def __init__(self, is_significant, comparison_interval_latencies_sample=None, | ||
compared_to_interval_latencies_sample=None, percentual_diff=None, comparison_interval_avg=None, | ||
compared_to_interval_avg=None): | ||
super().__init__('AVERAGE_LATENCY') | ||
|
||
self._is_significant = is_significant | ||
self._comparison_interval_latencies_sample = comparison_interval_latencies_sample | ||
self._compared_to_interval_latencies_sample = compared_to_interval_latencies_sample | ||
self._percentual_diff = percentual_diff | ||
|
||
self._compared_to_interval_avg = compared_to_interval_avg | ||
self._comparison_interval_avg = comparison_interval_avg | ||
|
||
def meta(self): | ||
return dict( | ||
latencies_sample=dict( | ||
comparison_interval=self._comparison_interval_latencies_sample, | ||
compared_to_interval=self._compared_to_interval_latencies_sample | ||
), | ||
comparison_average=self._comparison_interval_avg, | ||
compared_to_average=self._compared_to_interval_avg, | ||
percentual_diff=self._percentual_diff, | ||
) | ||
|
||
def is_significant(self): | ||
return self._is_significant | ||
|
||
|
||
class AverageLatency(ReportQuestion): | ||
|
||
def get_answer(self, endpoint, comparison_interval, compared_to_interval): | ||
with session_scope() as db_session: | ||
comparison_interval_latencies_sample = get_latencies_sample(db_session, endpoint.id, comparison_interval) | ||
compared_to_interval_latencies_sample = get_latencies_sample(db_session, endpoint.id, compared_to_interval) | ||
|
||
if len(comparison_interval_latencies_sample) == 0 or len(compared_to_interval_latencies_sample) == 0: | ||
return AverageLatencyAnswer(is_significant=False) | ||
|
||
comparison_interval_avg = np.average(comparison_interval_latencies_sample) | ||
compared_to_interval_avg = np.average(compared_to_interval_latencies_sample) | ||
|
||
percentual_diff = (comparison_interval_avg - compared_to_interval_avg) / compared_to_interval_avg * 100 | ||
|
||
p_value = mean_permutation_test(comparison_interval_latencies_sample, | ||
compared_to_interval_latencies_sample, | ||
num_rounds=1000) | ||
is_significant = abs(float(percentual_diff)) > 30 and p_value < 0.05 | ||
|
||
return AverageLatencyAnswer( | ||
is_significant=is_significant, | ||
|
||
percentual_diff=percentual_diff, | ||
|
||
# Sample latencies | ||
comparison_interval_latencies_sample=comparison_interval_latencies_sample, | ||
compared_to_interval_latencies_sample=compared_to_interval_latencies_sample, | ||
|
||
# Latency averages | ||
comparison_interval_avg=comparison_interval_avg, | ||
compared_to_interval_avg=compared_to_interval_avg | ||
) |
34 changes: 34 additions & 0 deletions
34
flask_monitoringdashboard/core/reporting/questions/report_question.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
|
||
from abc import ABCMeta, abstractmethod | ||
|
||
|
||
class Answer: | ||
__metaclass__ = ABCMeta | ||
|
||
def __init__(self, type): | ||
self.type = type | ||
|
||
@abstractmethod | ||
def is_significant(self): | ||
pass | ||
|
||
@abstractmethod | ||
def meta(self): | ||
pass | ||
|
||
def serialize(self): | ||
base = dict( | ||
is_significant=self.is_significant(), | ||
type=self.type | ||
) | ||
base.update(self.meta()) | ||
|
||
return base | ||
|
||
|
||
class ReportQuestion: | ||
__metaclass__ = ABCMeta | ||
|
||
@abstractmethod | ||
def get_answer(self, endpoint, comparison_interval, compared_to_interval): | ||
pass |
81 changes: 81 additions & 0 deletions
81
flask_monitoringdashboard/core/reporting/questions/status_code_distribution.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
from collections import defaultdict | ||
|
||
from flask_monitoringdashboard.controllers.requests import get_status_code_frequencies_in_interval | ||
from flask_monitoringdashboard.core.reporting.questions.report_question import Answer, ReportQuestion | ||
from flask_monitoringdashboard.database import session_scope | ||
|
||
|
||
class StatusCodeDistributionAnswer(Answer): | ||
def __init__(self, is_significant=False, percentages=None): | ||
super().__init__('STATUS_CODE_DISTRIBUTION') | ||
|
||
self.percentages = percentages | ||
self._is_significant = is_significant | ||
|
||
def is_significant(self): | ||
return self._is_significant | ||
|
||
def meta(self): | ||
return dict( | ||
percentages=self.percentages | ||
) | ||
|
||
|
||
def frequency_to_percentage(freq, total): | ||
if total == 0: | ||
raise ValueError('`total` can not be zero!') | ||
|
||
return (float(freq)) / total * 100 | ||
|
||
|
||
class StatusCodeDistribution(ReportQuestion): | ||
|
||
def get_answer(self, endpoint, comparison_interval, compared_to_interval): | ||
with session_scope() as db_session: | ||
comparison_interval_frequencies = get_status_code_frequencies_in_interval(db_session, endpoint.id, | ||
comparison_interval.start_date(), | ||
comparison_interval.end_date()) | ||
|
||
|
||
compared_to_interval_frequencies = get_status_code_frequencies_in_interval( | ||
db_session, endpoint.id, | ||
compared_to_interval.start_date(), | ||
compared_to_interval.end_date()) | ||
|
||
registered_status_codes = set(compared_to_interval_frequencies.keys()).union( | ||
set(comparison_interval_frequencies.keys())) | ||
|
||
total_requests_comparison_interval = sum(comparison_interval_frequencies.values()) | ||
total_requests_compared_to_interval = sum(compared_to_interval_frequencies.values()) | ||
|
||
if total_requests_comparison_interval == 0 or total_requests_compared_to_interval == 0: | ||
return StatusCodeDistributionAnswer(is_significant=False) | ||
|
||
percentages = [] | ||
max_absolute_diff = 0 | ||
|
||
for status_code in registered_status_codes: | ||
count_comparison_interval = comparison_interval_frequencies[ | ||
status_code] if status_code in comparison_interval_frequencies else 0 | ||
|
||
count_compared_to_interval = compared_to_interval_frequencies[ | ||
status_code] if status_code in compared_to_interval_frequencies else 0 | ||
|
||
comparison_interval_percentage = frequency_to_percentage(count_comparison_interval, | ||
total_requests_comparison_interval) | ||
|
||
compared_to_interval_percentage = frequency_to_percentage(count_compared_to_interval, | ||
total_requests_compared_to_interval) | ||
|
||
percentage_diff = comparison_interval_percentage - compared_to_interval_percentage | ||
|
||
percentages.append(dict( | ||
status_code=status_code, | ||
comparison_interval=comparison_interval_percentage, | ||
compared_to_interval=compared_to_interval_percentage, | ||
percentage_diff=percentage_diff | ||
)) | ||
|
||
max_absolute_diff = max(max_absolute_diff, percentage_diff) | ||
|
||
return StatusCodeDistributionAnswer(is_significant=max_absolute_diff > 3, percentages=percentages) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.