Spaces:
Sleeping
Sleeping
added prose and sequencing to the article
Browse files- app.py +182 -65
- concattedfiles.py +0 -381
- utils.py +24 -4
app.py
CHANGED
@@ -1,64 +1,168 @@
|
|
1 |
import streamlit as st
|
2 |
import pandas as pd
|
3 |
from datetime import time, date
|
4 |
-
from utils import generate_random_data, evaluate_alarm_state, aggregate_data, re_aggregate_data
|
5 |
from textwrap import dedent
|
6 |
from matplotlib import pyplot as plt
|
7 |
|
8 |
# Constants
|
9 |
-
|
10 |
|
11 |
def main():
|
12 |
-
st.title("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
# Initialize session state
|
15 |
initialize_session_state()
|
16 |
|
17 |
# Section 1 - Generate random data
|
18 |
-
st.header("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
generate_data_form()
|
20 |
|
21 |
if not st.session_state.df.empty:
|
22 |
-
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
|
25 |
-
|
26 |
-
st.header("Section 2 - Calculate Aggregations")
|
27 |
-
aggregation_form()
|
28 |
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
key='aggregation_function_input__alarm',
|
49 |
-
help="Select the aggregation function for visualizing the data."
|
50 |
-
)
|
51 |
-
st.line_chart(st.session_state.summary_by_period_df.set_index("Timestamp")[aggregation_function_input__alarm])
|
52 |
-
|
53 |
-
# Section 4 - Evaluate Alarm State
|
54 |
-
st.header("Section 4 - Evaluate Alarm State")
|
55 |
-
alarm_state_form()
|
56 |
-
|
57 |
-
if not st.session_state.alarm_state_df.empty:
|
58 |
plot_time_series(st.session_state.summary_by_period_df, st.session_state.threshold_input, st.session_state.alarm_condition_input, st.session_state.evaluation_range_input)
|
|
|
|
|
|
|
59 |
display_alarm_state_evaluation(st.session_state.alarm_state_df)
|
60 |
|
61 |
-
|
62 |
|
63 |
def initialize_session_state() -> None:
|
64 |
if 'df' not in st.session_state:
|
@@ -72,16 +176,16 @@ def initialize_session_state() -> None:
|
|
72 |
|
73 |
def generate_data_form() -> None:
|
74 |
with st.form(key='generate_data_form'):
|
75 |
-
start_time_input = st.time_input("Start Time", time(12, 0), help="
|
76 |
-
end_time_input = st.time_input("End Time", time(12, 30), help="
|
77 |
-
count_input = st.slider("
|
78 |
-
response_time_range_input = st.slider("Response Time Range (ms)", min_value=50, max_value=300, value=(
|
79 |
-
null_percentage_input = st.slider("
|
80 |
submit_button = st.form_submit_button(label='Generate Data')
|
81 |
|
82 |
if submit_button:
|
83 |
st.session_state.df = generate_random_data(
|
84 |
-
date=
|
85 |
start_time=start_time_input,
|
86 |
end_time=end_time_input,
|
87 |
count=count_input,
|
@@ -90,41 +194,54 @@ def generate_data_form() -> None:
|
|
90 |
)
|
91 |
|
92 |
def aggregation_form() -> None:
|
93 |
-
freq_input = st.selectbox("
|
94 |
if not st.session_state.df.empty:
|
95 |
st.session_state.aggregated_df = aggregate_data(st.session_state.df, freq_input)
|
96 |
|
97 |
def summary_by_period_form() -> None:
|
98 |
-
period_length_input = st.selectbox("Period Length", ['1min', '5min', '15min'], key='period_length_input', help="Select the period length for aggregating the summary data.")
|
99 |
if not st.session_state.aggregated_df.empty:
|
100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
101 |
|
102 |
def alarm_state_form() -> None:
|
103 |
-
threshold_input = st.slider("Threshold (ms)", min_value=50, max_value=300, value=
|
104 |
-
datapoints_to_alarm_input = st.number_input("Datapoints to Alarm", min_value=1, value=3, key='datapoints_to_alarm_input', help="Specify the number of data points required to trigger an alarm.")
|
105 |
-
evaluation_range_input = st.number_input("Evaluation Range", min_value=1, value=5, key='evaluation_range_input', help="Specify the range of data points to evaluate for alarm state.")
|
106 |
alarm_condition_input = st.selectbox(
|
107 |
"Alarm Condition",
|
108 |
['>', '>=', '<', '<='],
|
109 |
key='alarm_condition_input',
|
110 |
help="Select the condition for evaluating the alarm state."
|
111 |
)
|
|
|
|
|
|
|
|
|
112 |
if not st.session_state.summary_by_period_df.empty:
|
113 |
st.session_state.alarm_state_df = evaluate_alarm_state(
|
114 |
summary_df=st.session_state.summary_by_period_df,
|
115 |
-
threshold=threshold_input,
|
116 |
-
datapoints_to_alarm=datapoints_to_alarm_input,
|
117 |
-
evaluation_range=evaluation_range_input,
|
118 |
aggregation_function=st.session_state.aggregation_function_input__alarm,
|
119 |
-
alarm_condition=alarm_condition_input
|
120 |
-
)
|
121 |
|
122 |
def display_dataframe(title: str, df: pd.DataFrame) -> None:
|
123 |
st.write(title)
|
124 |
st.dataframe(df)
|
125 |
|
126 |
def plot_time_series(df: pd.DataFrame, threshold: int, alarm_condition: str, evaluation_range: int) -> None:
|
127 |
-
timestamps = df['
|
128 |
response_times = df[st.session_state.aggregation_function_input__alarm]
|
129 |
|
130 |
segments = []
|
@@ -145,7 +262,7 @@ def plot_time_series(df: pd.DataFrame, threshold: int, alarm_condition: str, eva
|
|
145 |
fig, ax1 = plt.subplots()
|
146 |
|
147 |
color = 'tab:blue'
|
148 |
-
ax1.set_xlabel('
|
149 |
ax1.set_ylabel('Response Time (ms)', color=color)
|
150 |
|
151 |
for segment in segments:
|
@@ -170,11 +287,11 @@ def plot_time_series(df: pd.DataFrame, threshold: int, alarm_condition: str, eva
|
|
170 |
|
171 |
for idx in period_indices:
|
172 |
if idx % evaluation_range == 0:
|
173 |
-
ax1.axvline(x=df['
|
174 |
max_value = max(filter(lambda x: x is not None, df[st.session_state.aggregation_function_input__alarm]))
|
175 |
-
ax1.text(df['
|
176 |
else:
|
177 |
-
ax1.axvline(x=df['
|
178 |
|
179 |
ax1.annotate('Alarm threshold', xy=(0.98, threshold), xycoords=('axes fraction', 'data'), ha='right', va='bottom', fontsize=8, color='red', backgroundcolor='none')
|
180 |
|
@@ -193,16 +310,17 @@ def display_key_tables() -> None:
|
|
193 |
symbol_data = {
|
194 |
"Symbol": ["🔴", "⚫️", "🟢"],
|
195 |
"Meaning": [
|
196 |
-
"Breaching data point: This data point
|
197 |
-
"Missing data point: This data point is missing or not reported
|
198 |
-
"Non-breaching data point: This data point is
|
199 |
]
|
200 |
}
|
201 |
symbol_df = pd.DataFrame(symbol_data)
|
202 |
st.table(symbol_df)
|
203 |
|
204 |
# Columns
|
205 |
-
st.write(dedent("""
|
|
|
206 |
|
207 |
Sometimes, no metric events may have been reported during a given time period. In this case,
|
208 |
you must decide how you will treat missing data points. Ignore it? Or consider it a failure.
|
@@ -238,4 +356,3 @@ def display_key_tables() -> None:
|
|
238 |
|
239 |
if __name__ == "__main__":
|
240 |
main()
|
241 |
-
|
|
|
1 |
import streamlit as st
|
2 |
import pandas as pd
|
3 |
from datetime import time, date
|
4 |
+
from utils import generate_random_data, evaluate_alarm_state, aggregate_data, re_aggregate_data, downsample
|
5 |
from textwrap import dedent
|
6 |
from matplotlib import pyplot as plt
|
7 |
|
8 |
# Constants
|
9 |
+
TODAYS_DATE = date.today()
|
10 |
|
11 |
def main():
|
12 |
+
st.title("AWS CloudWatch Simulator")
|
13 |
+
st.markdown(dedent("""\
|
14 |
+
Monitoring and alerting can be confusing to learn. There is some theory you need to understand first.
|
15 |
+
|
16 |
+
This app is an interative tutorial to help you understand how to record metrics describing the performance
|
17 |
+
of an app, and build alerts off of them using AWS CloudWatch.
|
18 |
+
|
19 |
+
Lets get started! 🎉
|
20 |
+
"""))
|
21 |
|
22 |
# Initialize session state
|
23 |
initialize_session_state()
|
24 |
|
25 |
# Section 1 - Generate random data
|
26 |
+
st.header("1 - Generate a series of measurements")
|
27 |
+
st.markdown(dedent("""\
|
28 |
+
Suppose we have a REST API with a ✨very popular✨ `GET /greeting?name=...` endpoint.
|
29 |
+
|
30 |
+
Each time someone calls the endpoint, we can record how long it takes to respond, aka the ***response latency***.
|
31 |
+
|
32 |
+
Use this form to generate a random dataset of response times.
|
33 |
+
"""))
|
34 |
+
|
35 |
generate_data_form()
|
36 |
|
37 |
if not st.session_state.df.empty:
|
38 |
+
st.markdown("### Recorded request latencies")
|
39 |
+
display_dataframe("Raw timeseries events", st.session_state.df)
|
40 |
+
st.scatter_chart(st.session_state.df.set_index("timestamp"))
|
41 |
+
|
42 |
+
st.markdown(dedent("""\
|
43 |
+
#### 🚚 ➡ ☁️
|
44 |
+
We can ship these metrics to a time series database such as AWS CloudWatch in a few ways.
|
45 |
+
"""))
|
46 |
+
st.warning("In the CloudWatch Metrics database, data points are organized into [Namespaces, Metrics, and Dimensions](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Namespace). Think of a Metric as a dedicated table in a database for a single timeseries, e.g. response latency measurements.", icon="💡")
|
47 |
+
st.markdown(dedent("""\
|
48 |
+
#### Option 1 - AWS SDK (good)
|
49 |
+
|
50 |
+
Our application could use the AWS SDK to upload the data points using the AWS CloudWatch endpoints, e.g.
|
51 |
+
|
52 |
+
```python
|
53 |
+
import boto3
|
54 |
+
cloudwatch = boto3.client('cloudwatch')
|
55 |
+
cloudwatch.put_metric_data(
|
56 |
+
Namespace='MyApp',
|
57 |
+
MetricData=[
|
58 |
+
{
|
59 |
+
'MetricName': 'Latency',
|
60 |
+
'timestamp': '2021-08-01T12:00:00',
|
61 |
+
'Value': 102,
|
62 |
+
'Unit': 'Milliseconds'
|
63 |
+
},
|
64 |
+
... # more metrics data points, recorded at different times
|
65 |
+
]
|
66 |
+
)
|
67 |
+
```
|
68 |
+
|
69 |
+
It is more cost effective to send data points in a batch, but they can be sent individually as well.
|
70 |
+
|
71 |
+
---
|
72 |
+
|
73 |
+
#### Option 2 - Structured Logs (better)
|
74 |
+
|
75 |
+
Our application could write metrics to stdout in AWS's [Embedded Metric Format (EMF)](https://www.youtube.com/watch?v=HdopVzW6pX0) (structured JSON) and sent to CloudWatch Logs.
|
76 |
+
|
77 |
+
CloudWatch logs automatically extracts metrics from EMF-formatted logs and sends them to CloudWatch Metrics.
|
78 |
+
|
79 |
+
That is great because it is
|
80 |
+
|
81 |
+
1. 💰 **cheaper**: you are not charged for calls to CloudWatch's PutMetric endpoint, and
|
82 |
+
2. ⚡️ **faster**: logging to stdout is WAY faster than making a network call--especially a 2-way, synchonous HTTP call. And a side process can batch and send our logs without our app having to slow down or worry about that.
|
83 |
+
|
84 |
+
---
|
85 |
+
|
86 |
+
### Option 3 - Built-in Metrics (best)
|
87 |
+
|
88 |
+
Some common metrics, such as API Gateway response latency or Lambda runtime can actually be recorded
|
89 |
+
in CloudWatch Metrics automatically. No code required!
|
90 |
+
|
91 |
+
This is ideal, but not all metrics are automatically captured, such as application-specific metrics like "how many OpenAI tokens have we used?"
|
92 |
+
|
93 |
+
---
|
94 |
+
"""))
|
95 |
|
96 |
+
if not st.session_state.df.empty:
|
|
|
|
|
97 |
|
98 |
+
# Section 2 - Calculate Aggregations
|
99 |
+
st.header("2 - AWS aggregates the metrics")
|
100 |
+
st.markdown(dedent("""\
|
101 |
+
This step represents our metrics data after AWS CloudWatch processes and stores it.
|
102 |
+
|
103 |
+
Storing raw metrics data can be expensive 💰 (see [CloudWatch Metrics pricing](https://aws.amazon.com/cloudwatch/pricing/)). If your app has high traffic, or bad code, you could send 100s, 1,000s, or 1,000,000s+ of measurement
|
104 |
+
data points per second to AWS CloudWatch.
|
105 |
+
|
106 |
+
This metrics data is meant to be analyzed with queries that power visualizations and alerts--which requires compute--which costs more money the more metrics data you have stored.
|
107 |
+
|
108 |
+
AWS CloudWatch generally aggregates data into a ***resolution*** of 5 minute intervals.
|
109 |
+
|
110 |
+
In other words, CloudWatch bins data, genrally into ***periods*** of 5 minutes
|
111 |
+
and only stores aggregate statistics for each period. This decreases the amount of data stored and queried by orders of magnitude. ✅
|
112 |
+
|
113 |
+
You can pay more for AWS to aggregate data at a "higher" (or "finer") resolution, e.g. 1-minute or even 1-second periods.
|
114 |
+
"""))
|
115 |
+
st.info("Use this form to aggregate the raw data points into periods of different lengths and plot some of [the many statistics that CloudWatch computes](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Statistics-definitions.html) over aggregated periods.", icon="📌")
|
116 |
+
aggregation_form()
|
117 |
+
|
118 |
+
if not st.session_state.aggregated_df.empty:
|
119 |
+
display_dataframe("Aggregated Statistics over Periods", st.session_state.aggregated_df)
|
120 |
+
aggregation_function_input__storage = st.selectbox(
|
121 |
+
"Aggregation Statistic (just for exploration; does not affect downstream steps)",
|
122 |
+
['p50', 'p95', 'p99', 'max', 'min', 'average'],
|
123 |
+
key='aggregation_function_input__storage',
|
124 |
+
help="Select the aggregation function for visualizing the data."
|
125 |
+
)
|
126 |
+
st.line_chart(st.session_state.aggregated_df.set_index("timestamp")[aggregation_function_input__storage])
|
127 |
+
|
128 |
+
# Section 3 - Summary Data Aggregated by Period
|
129 |
+
st.header("3 - Optionally aggregate metrics further for Alarms and Dashboards")
|
130 |
+
st.markdown(dedent("""\
|
131 |
+
You can plot metrics in a CloudWatch dashboard.
|
132 |
+
|
133 |
+
When doing this, you can choose to aggregate the data further or run additional queries on it to analyze it and answer particular questions.
|
134 |
+
|
135 |
+
We will skip discussing dashboards and focus on ***alerts*** (or ***alarms*** in CloudWatch terms).
|
136 |
+
|
137 |
+
Suppose we want an alert that triggers if our endpoint starts to take longer than usual to respond.
|
138 |
+
|
139 |
+
CloudWatch's concept of alarms can alert you when a metric, such as response latency, "breaches" a certain *threshold* for a certain *number of periods*.
|
140 |
+
"""))
|
141 |
+
st.info("Use this form to bin the data into periods (optionally of shorter length than the previous step).\n\nThis will set the period length used to create an alarm in the next step.", icon="📌")
|
142 |
+
summary_by_period_form()
|
143 |
+
|
144 |
+
if not st.session_state.summary_by_period_df.empty:
|
145 |
+
display_dataframe("Summary Data Aggregated by Period (for Alarm)", st.session_state.summary_by_period_df)
|
146 |
+
aggregation_function_input__alarm = st.selectbox(
|
147 |
+
"Aggregation Statistic (used for alarm evaluation in next step)",
|
148 |
+
['p50', 'p95', 'p99', 'max', 'min', 'average'],
|
149 |
+
key='aggregation_function_input__alarm',
|
150 |
+
help="Select the aggregation function for visualizing the data."
|
151 |
+
)
|
152 |
+
st.line_chart(st.session_state.summary_by_period_df.set_index("timestamp")[aggregation_function_input__alarm])
|
153 |
|
154 |
+
# Section 4 - Evaluate Alarm State
|
155 |
+
st.header("4 - Configure and evaluate an alarm")
|
156 |
+
|
157 |
+
# define what "breaching" means (threshold and condition) and evaluate the data
|
158 |
+
alarm_state_form()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
159 |
plot_time_series(st.session_state.summary_by_period_df, st.session_state.threshold_input, st.session_state.alarm_condition_input, st.session_state.evaluation_range_input)
|
160 |
+
datapoints_to_alarm_input = st.number_input("Datapoints to Alarm", min_value=1, value=3, key='datapoints_to_alarm_input', help="Specify the number of data points with in the overall evaluation range that must be breaching in order to trigger an alarm.")
|
161 |
+
evaluate_breaching_data_points()
|
162 |
+
st.write("%d out of %d data points must be breaching to trigger an alarm." % (st.session_state.datapoints_to_alarm_input, st.session_state.evaluation_range_input))
|
163 |
display_alarm_state_evaluation(st.session_state.alarm_state_df)
|
164 |
|
165 |
+
display_key_tables()
|
166 |
|
167 |
def initialize_session_state() -> None:
|
168 |
if 'df' not in st.session_state:
|
|
|
176 |
|
177 |
def generate_data_form() -> None:
|
178 |
with st.form(key='generate_data_form'):
|
179 |
+
start_time_input = st.time_input("Start Time", time(12, 0), help="No generated data points will have earlier timestamps than this.")
|
180 |
+
end_time_input = st.time_input("End Time", time(12, 30), help="No generated data points will have later timestamps than this.")
|
181 |
+
count_input = st.slider("Number of requests", min_value=1, max_value=200, value=20, help="Specify the number of data points to generate.")
|
182 |
+
response_time_range_input = st.slider("Response Time Range (ms)", min_value=50, max_value=300, value=(140, 180), help="Select the range of response times in milliseconds. The generated response latencies will be in this range.")
|
183 |
+
null_percentage_input = st.slider("Percentage of null data points", min_value=0.0, max_value=1.0, value=0., help="Select the percentage of null values in the generated data. We will use this to simulate 'missing data'--or time periods where no requests were recorded.\n\nCloudWatch does not actually have a concept of data points with null values.")
|
184 |
submit_button = st.form_submit_button(label='Generate Data')
|
185 |
|
186 |
if submit_button:
|
187 |
st.session_state.df = generate_random_data(
|
188 |
+
date=TODAYS_DATE,
|
189 |
start_time=start_time_input,
|
190 |
end_time=end_time_input,
|
191 |
count=count_input,
|
|
|
194 |
)
|
195 |
|
196 |
def aggregation_form() -> None:
|
197 |
+
freq_input = st.selectbox("Storage resolution for metric", ['1min', '2min', '3min', '5min', '10min', '15min'], key='freq_input', help="Select the frequency for aggregating the data.")
|
198 |
if not st.session_state.df.empty:
|
199 |
st.session_state.aggregated_df = aggregate_data(st.session_state.df, freq_input)
|
200 |
|
201 |
def summary_by_period_form() -> None:
|
202 |
+
period_length_input = st.selectbox("Period Length", ['1min', '2min', '3min', '5min', '10min', '15min'], key='period_length_input', help="Select the period length for aggregating the summary data.")
|
203 |
if not st.session_state.aggregated_df.empty:
|
204 |
+
agg_period = int(st.session_state.freq_input.replace('min', ''))
|
205 |
+
new_period = int(period_length_input.replace('min', ''))
|
206 |
+
|
207 |
+
if new_period < agg_period:
|
208 |
+
st.warning(f"The data from Step 2 was downsampled from a {agg_period}-minute resolution to a {new_period}-minute resolution.\n\nRepresentative values for each finer-resolution period were interpolated.", icon="📌")
|
209 |
+
elif new_period > agg_period:
|
210 |
+
st.warning(f"The data from Step 2 was re-aggregated to a lower resolution (longer period) of {new_period} minutes.\n\nThe resulting values for min, max, and average reflect the values of the collected metrics, but p50, p95, and p99 are merely estimates.", icon="📌")
|
211 |
+
|
212 |
+
if new_period < agg_period:
|
213 |
+
st.session_state.summary_by_period_df = downsample(st.session_state.aggregated_df, new_period)
|
214 |
+
else:
|
215 |
+
st.session_state.summary_by_period_df = re_aggregate_data(st.session_state.aggregated_df, period_length_input)
|
216 |
|
217 |
def alarm_state_form() -> None:
|
218 |
+
threshold_input = st.slider("Threshold (ms)", min_value=50, max_value=300, value=160, key='threshold_input', help="Specify the threshold value for evaluating the alarm state.")
|
|
|
|
|
219 |
alarm_condition_input = st.selectbox(
|
220 |
"Alarm Condition",
|
221 |
['>', '>=', '<', '<='],
|
222 |
key='alarm_condition_input',
|
223 |
help="Select the condition for evaluating the alarm state."
|
224 |
)
|
225 |
+
|
226 |
+
evaluation_range_input = st.number_input("Evaluation Range (# periods btw green bars)", min_value=1, value=5, key='evaluation_range_input', help="Specify the number of consecutive data points to evaluate for alarm state.")
|
227 |
+
|
228 |
+
def evaluate_breaching_data_points() -> None:
|
229 |
if not st.session_state.summary_by_period_df.empty:
|
230 |
st.session_state.alarm_state_df = evaluate_alarm_state(
|
231 |
summary_df=st.session_state.summary_by_period_df,
|
232 |
+
threshold=st.session_state.threshold_input,
|
233 |
+
datapoints_to_alarm=st.session_state.datapoints_to_alarm_input,
|
234 |
+
evaluation_range=st.session_state.evaluation_range_input,
|
235 |
aggregation_function=st.session_state.aggregation_function_input__alarm,
|
236 |
+
alarm_condition=st.session_state.alarm_condition_input
|
237 |
+
)
|
238 |
|
239 |
def display_dataframe(title: str, df: pd.DataFrame) -> None:
|
240 |
st.write(title)
|
241 |
st.dataframe(df)
|
242 |
|
243 |
def plot_time_series(df: pd.DataFrame, threshold: int, alarm_condition: str, evaluation_range: int) -> None:
|
244 |
+
timestamps = df['timestamp']
|
245 |
response_times = df[st.session_state.aggregation_function_input__alarm]
|
246 |
|
247 |
segments = []
|
|
|
262 |
fig, ax1 = plt.subplots()
|
263 |
|
264 |
color = 'tab:blue'
|
265 |
+
ax1.set_xlabel('timestamp')
|
266 |
ax1.set_ylabel('Response Time (ms)', color=color)
|
267 |
|
268 |
for segment in segments:
|
|
|
287 |
|
288 |
for idx in period_indices:
|
289 |
if idx % evaluation_range == 0:
|
290 |
+
ax1.axvline(x=df['timestamp'].iloc[idx], color='green', linestyle='-', alpha=0.3)
|
291 |
max_value = max(filter(lambda x: x is not None, df[st.session_state.aggregation_function_input__alarm]))
|
292 |
+
ax1.text(df['timestamp'].iloc[idx], max_value * 0.95, f"[{idx // evaluation_range}]", rotation=90, verticalalignment='bottom', color='grey', alpha=0.7, fontsize=8)
|
293 |
else:
|
294 |
+
ax1.axvline(x=df['timestamp'].iloc[idx], color='grey', linestyle='--', alpha=0.3)
|
295 |
|
296 |
ax1.annotate('Alarm threshold', xy=(0.98, threshold), xycoords=('axes fraction', 'data'), ha='right', va='bottom', fontsize=8, color='red', backgroundcolor='none')
|
297 |
|
|
|
310 |
symbol_data = {
|
311 |
"Symbol": ["🔴", "⚫️", "🟢"],
|
312 |
"Meaning": [
|
313 |
+
"Breaching data point: This data point breaches the threshold and alarm condition (<, <=, >=, >)",
|
314 |
+
"Missing data point: This data point is missing or not reported",
|
315 |
+
"Non-breaching data point: This data point is does not breach the threshold and alarm condition (<, <=, >=, >)"
|
316 |
]
|
317 |
}
|
318 |
symbol_df = pd.DataFrame(symbol_data)
|
319 |
st.table(symbol_df)
|
320 |
|
321 |
# Columns
|
322 |
+
st.write(dedent("""\
|
323 |
+
#### Columns: [The 4 Strategies](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#alarms-and-missing-data) for handling missing data points
|
324 |
|
325 |
Sometimes, no metric events may have been reported during a given time period. In this case,
|
326 |
you must decide how you will treat missing data points. Ignore it? Or consider it a failure.
|
|
|
356 |
|
357 |
if __name__ == "__main__":
|
358 |
main()
|
|
concattedfiles.py
DELETED
@@ -1,381 +0,0 @@
|
|
1 |
-
.
|
2 |
-
├── streamlit_app.py
|
3 |
-
└── utils.py
|
4 |
-
|
5 |
-
1 directory, 2 files
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
# File: ./streamlit_app.py
|
10 |
-
import streamlit as st
|
11 |
-
import pandas as pd
|
12 |
-
import matplotlib.pyplot as plt
|
13 |
-
from datetime import datetime, time, date
|
14 |
-
from typing import List, Dict, Any, Tuple
|
15 |
-
from utils import generate_random_data, calculate_percentile, evaluate_alarm_state, aggregate_data
|
16 |
-
|
17 |
-
# Constants
|
18 |
-
HARD_CODED_DATE = date(2024, 7, 26)
|
19 |
-
|
20 |
-
def main():
|
21 |
-
st.title("Streamlit App for Data Generation and Analysis")
|
22 |
-
|
23 |
-
# Initialize session state
|
24 |
-
initialize_session_state()
|
25 |
-
|
26 |
-
# Section 1 - Generate random data
|
27 |
-
st.header("Section 1 - Generate Random Data")
|
28 |
-
generate_data_form()
|
29 |
-
|
30 |
-
if not st.session_state.df.empty:
|
31 |
-
display_dataframe("Raw Event Data", st.session_state.df)
|
32 |
-
|
33 |
-
# Section 2 - Calculate Percentile
|
34 |
-
st.header("Section 2 - Calculate Percentile")
|
35 |
-
percentile_form()
|
36 |
-
|
37 |
-
if not st.session_state.percentile_df.empty:
|
38 |
-
display_dataframe("Aggregated Summary Data", st.session_state.percentile_df)
|
39 |
-
|
40 |
-
# Section 3 - Summary Data Aggregated by Period
|
41 |
-
st.header("Section 3 - Summary Data Aggregated by Period")
|
42 |
-
summary_by_period_form()
|
43 |
-
|
44 |
-
if not st.session_state.summary_by_period_df.empty:
|
45 |
-
display_dataframe("Summary Data Aggregated by Period", st.session_state.summary_by_period_df)
|
46 |
-
|
47 |
-
# Section 4 - Evaluate Alarm State
|
48 |
-
st.header("Section 4 - Evaluate Alarm State")
|
49 |
-
alarm_state_form()
|
50 |
-
|
51 |
-
if not st.session_state.alarm_state_df.empty:
|
52 |
-
plot_time_series(st.session_state.summary_by_period_df, st.session_state.threshold_input, st.session_state.alarm_condition_input, st.session_state.evaluation_range_input)
|
53 |
-
display_alarm_state_evaluation(st.session_state.alarm_state_df)
|
54 |
-
|
55 |
-
display_key_tables()
|
56 |
-
|
57 |
-
def initialize_session_state() -> None:
|
58 |
-
if 'df' not in st.session_state:
|
59 |
-
st.session_state.df = pd.DataFrame()
|
60 |
-
if 'percentile_df' not in st.session_state:
|
61 |
-
st.session_state.percentile_df = pd.DataFrame()
|
62 |
-
if 'summary_by_period_df' not in st.session_state:
|
63 |
-
st.session_state.summary_by_period_df = pd.DataFrame()
|
64 |
-
if 'alarm_state_df' not in st.session_state:
|
65 |
-
st.session_state.alarm_state_df = pd.DataFrame()
|
66 |
-
|
67 |
-
def generate_data_form() -> None:
|
68 |
-
with st.form(key='generate_data_form'):
|
69 |
-
start_time_input = st.time_input("Start Time", time(12, 0), help="Select the start time for generating random data.")
|
70 |
-
end_time_input = st.time_input("End Time", time(12, 30), help="Select the end time for generating random data.")
|
71 |
-
count_input = st.slider("Count", min_value=1, max_value=200, value=60, help="Specify the number of data points to generate.")
|
72 |
-
response_time_range_input = st.slider("Response Time Range (ms)", min_value=50, max_value=300, value=(100, 250), help="Select the range of response times in milliseconds.")
|
73 |
-
null_percentage_input = st.slider("Null Percentage", min_value=0.0, max_value=1.0, value=0.5, help="Select the percentage of null values in the generated data.")
|
74 |
-
submit_button = st.form_submit_button(label='Generate Data')
|
75 |
-
|
76 |
-
if submit_button:
|
77 |
-
st.session_state.df = generate_random_data(
|
78 |
-
date=HARD_CODED_DATE,
|
79 |
-
start_time=start_time_input,
|
80 |
-
end_time=end_time_input,
|
81 |
-
count=count_input,
|
82 |
-
response_time_range=response_time_range_input,
|
83 |
-
null_percentage=null_percentage_input
|
84 |
-
)
|
85 |
-
|
86 |
-
def percentile_form() -> None:
|
87 |
-
freq_input = st.selectbox("Period (bin)", ['1min', '5min', '15min'], key='freq_input', help="Select the frequency for aggregating the data.")
|
88 |
-
percentile_input = st.slider("Percentile", min_value=0.0, max_value=1.0, value=0.95, key='percentile_input', help="Select the percentile for calculating the aggregated summary data.")
|
89 |
-
if not st.session_state.df.empty:
|
90 |
-
st.session_state.percentile_df = calculate_percentile(st.session_state.df, freq_input, percentile_input)
|
91 |
-
|
92 |
-
def summary_by_period_form() -> None:
|
93 |
-
period_length_input = st.selectbox("Period Length", ['1min', '5min', '15min'], key='period_length_input', help="Select the period length for aggregating the summary data.")
|
94 |
-
if not st.session_state.df.empty:
|
95 |
-
st.session_state.summary_by_period_df = aggregate_data(st.session_state.df, period_length_input)
|
96 |
-
|
97 |
-
def alarm_state_form() -> None:
|
98 |
-
threshold_input = st.number_input("Threshold (ms)", min_value=50, max_value=300, value=150, key='threshold_input', help="Specify the threshold value for evaluating the alarm state.")
|
99 |
-
datapoints_to_alarm_input = st.number_input("Datapoints to Alarm", min_value=1, value=3, key='datapoints_to_alarm_input', help="Specify the number of data points required to trigger an alarm.")
|
100 |
-
evaluation_range_input = st.number_input("Evaluation Range", min_value=1, value=5, key='evaluation_range_input', help="Specify the range of data points to evaluate for alarm state.")
|
101 |
-
aggregation_function_input = st.selectbox(
|
102 |
-
"Aggregation Function",
|
103 |
-
['p50', 'p95', 'p99', 'max', 'min', 'average'],
|
104 |
-
key='aggregation_function_input',
|
105 |
-
help="Select the aggregation function for visualizing the data and computing alarms."
|
106 |
-
)
|
107 |
-
alarm_condition_input = st.selectbox(
|
108 |
-
"Alarm Condition",
|
109 |
-
['>', '>=', '<', '<='],
|
110 |
-
key='alarm_condition_input',
|
111 |
-
help="Select the condition for evaluating the alarm state."
|
112 |
-
)
|
113 |
-
if not st.session_state.summary_by_period_df.empty:
|
114 |
-
st.session_state.alarm_state_df = evaluate_alarm_state(
|
115 |
-
summary_df=st.session_state.summary_by_period_df,
|
116 |
-
threshold=threshold_input,
|
117 |
-
datapoints_to_alarm=datapoints_to_alarm_input,
|
118 |
-
evaluation_range=evaluation_range_input,
|
119 |
-
aggregation_function=aggregation_function_input,
|
120 |
-
alarm_condition=alarm_condition_input
|
121 |
-
)
|
122 |
-
|
123 |
-
def display_dataframe(title: str, df: pd.DataFrame) -> None:
|
124 |
-
st.write(title)
|
125 |
-
st.dataframe(df)
|
126 |
-
|
127 |
-
def plot_time_series(df: pd.DataFrame, threshold: int, alarm_condition: str, evaluation_range: int) -> None:
|
128 |
-
timestamps = df['Timestamp']
|
129 |
-
response_times = df[st.session_state.aggregation_function_input]
|
130 |
-
|
131 |
-
segments = []
|
132 |
-
current_segment = {'timestamps': [], 'values': []}
|
133 |
-
|
134 |
-
for timestamp, value in zip(timestamps, response_times):
|
135 |
-
if pd.isna(value):
|
136 |
-
if current_segment['timestamps']:
|
137 |
-
segments.append(current_segment)
|
138 |
-
current_segment = {'timestamps': [], 'values': []}
|
139 |
-
else:
|
140 |
-
current_segment['timestamps'].append(timestamp)
|
141 |
-
current_segment['values'].append(value)
|
142 |
-
|
143 |
-
if current_segment['timestamps']:
|
144 |
-
segments.append(current_segment)
|
145 |
-
|
146 |
-
fig, ax1 = plt.subplots()
|
147 |
-
|
148 |
-
color = 'tab:blue'
|
149 |
-
ax1.set_xlabel('Timestamp')
|
150 |
-
ax1.set_ylabel('Response Time (ms)', color=color)
|
151 |
-
|
152 |
-
for segment in segments:
|
153 |
-
ax1.plot(segment['timestamps'], segment['values'], color=color, linewidth=0.5)
|
154 |
-
ax1.scatter(segment['timestamps'], segment['values'], color=color, s=10)
|
155 |
-
|
156 |
-
line_style = '--' if alarm_condition in ['<', '>'] else '-'
|
157 |
-
ax1.axhline(y=threshold, color='r', linestyle=line_style, linewidth=0.8, label='Threshold')
|
158 |
-
ax1.tick_params(axis='y', labelcolor=color)
|
159 |
-
|
160 |
-
if alarm_condition in ['<=', '<']:
|
161 |
-
ax1.fill_between(timestamps, 0, threshold, color='pink', alpha=0.3)
|
162 |
-
else:
|
163 |
-
ax1.fill_between(timestamps, threshold, response_times.max(), color='pink', alpha=0.3)
|
164 |
-
|
165 |
-
period_indices = range(len(df))
|
166 |
-
ax2 = ax1.twiny()
|
167 |
-
ax2.set_xticks(period_indices)
|
168 |
-
ax2.set_xticklabels(period_indices, fontsize=8)
|
169 |
-
ax2.set_xlabel('Time Periods', fontsize=8)
|
170 |
-
ax2.xaxis.set_tick_params(width=0.5)
|
171 |
-
|
172 |
-
for idx in period_indices:
|
173 |
-
if idx % evaluation_range == 0:
|
174 |
-
ax1.axvline(x=df['Timestamp'].iloc[idx], color='green', linestyle='-', alpha=0.3)
|
175 |
-
max_value = max(filter(lambda x: x is not None, df[st.session_state.aggregation_function_input]))
|
176 |
-
ax1.text(df['Timestamp'].iloc[idx], max_value * 0.95, f"[{idx // evaluation_range}]", rotation=90, verticalalignment='bottom', color='grey', alpha=0.7, fontsize=8)
|
177 |
-
else:
|
178 |
-
ax1.axvline(x=df['Timestamp'].iloc[idx], color='grey', linestyle='--', alpha=0.3)
|
179 |
-
|
180 |
-
ax1.annotate('Alarm threshold', xy=(0.98, threshold), xycoords=('axes fraction', 'data'), ha='right', va='bottom', fontsize=8, color='red', backgroundcolor='none')
|
181 |
-
|
182 |
-
fig.tight_layout()
|
183 |
-
st.pyplot(fig)
|
184 |
-
|
185 |
-
def display_alarm_state_evaluation(df: pd.DataFrame) -> None:
|
186 |
-
st.write("Alarm State Evaluation")
|
187 |
-
st.dataframe(df)
|
188 |
-
|
189 |
-
def display_key_tables() -> None:
|
190 |
-
st.write("### Key")
|
191 |
-
|
192 |
-
# Symbols
|
193 |
-
st.write("#### Symbols")
|
194 |
-
symbol_data = {
|
195 |
-
"Symbol": ["X", "-", "0"],
|
196 |
-
"Meaning": [
|
197 |
-
"Breaching data point: This data point exceeds the threshold.",
|
198 |
-
"Missing data point: This data point is missing or not reported.",
|
199 |
-
"Non-breaching data point: This data point is within the threshold."
|
200 |
-
]
|
201 |
-
}
|
202 |
-
symbol_df = pd.DataFrame(symbol_data)
|
203 |
-
st.table(symbol_df)
|
204 |
-
|
205 |
-
# Columns
|
206 |
-
st.write("#### Columns")
|
207 |
-
column_data = {
|
208 |
-
"Column": ["MISSING", "IGNORE", "BREACHING", "NOT BREACHING"],
|
209 |
-
"Meaning": [
|
210 |
-
"Action to take when all data points are missing. Possible values: INSUFFICIENT_DATA, Retain current state, ALARM, OK.",
|
211 |
-
"Action to take when data points are missing but ignored. Possible values: Retain current state, ALARM, OK.",
|
212 |
-
"Action to take when missing data points are treated as breaching. Possible values: ALARM, OK.",
|
213 |
-
"Action to take when missing data points are treated as not breaching. Possible values: ALARM, OK."
|
214 |
-
]
|
215 |
-
}
|
216 |
-
column_df = pd.DataFrame(column_data)
|
217 |
-
st.table(column_df)
|
218 |
-
|
219 |
-
# States
|
220 |
-
st.write("#### States")
|
221 |
-
state_data = {
|
222 |
-
"State": ["ALARM", "OK", "Retain current state", "INSUFFICIENT_DATA"],
|
223 |
-
"Description": [
|
224 |
-
"Alarm state is triggered.",
|
225 |
-
"Everything is within the threshold.",
|
226 |
-
"The current alarm state is maintained.",
|
227 |
-
"Not enough data to make a determination."
|
228 |
-
]
|
229 |
-
}
|
230 |
-
state_df = pd.DataFrame(state_data)
|
231 |
-
st.table(state_df)
|
232 |
-
|
233 |
-
if __name__ == "__main__":
|
234 |
-
main()
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
# File: ./utils.py
|
239 |
-
import random
|
240 |
-
from datetime import datetime, timedelta, date, time
|
241 |
-
import pandas as pd
|
242 |
-
import numpy as np
|
243 |
-
from typing import List, Iterator, Dict, Any, Optional
|
244 |
-
|
245 |
-
def generate_random_data(
|
246 |
-
date: date,
|
247 |
-
start_time: time,
|
248 |
-
end_time: time,
|
249 |
-
count: int,
|
250 |
-
response_time_range: (int, int),
|
251 |
-
null_percentage: float
|
252 |
-
) -> pd.DataFrame:
|
253 |
-
start_datetime: datetime = datetime.combine(date, start_time)
|
254 |
-
end_datetime: datetime = datetime.combine(date, end_time)
|
255 |
-
|
256 |
-
random_timestamps: List[datetime] = [
|
257 |
-
start_datetime + timedelta(seconds=random.randint(0, int((end_datetime - start_datetime).total_seconds())))
|
258 |
-
for _ in range(count)
|
259 |
-
]
|
260 |
-
random_timestamps.sort()
|
261 |
-
|
262 |
-
random_response_times: List[Optional[int]] = [
|
263 |
-
random.randint(response_time_range[0], response_time_range[1]) for _ in range(count)
|
264 |
-
]
|
265 |
-
|
266 |
-
null_count: int = int(null_percentage * count)
|
267 |
-
null_indices: List[int] = random.sample(range(count), null_count)
|
268 |
-
for idx in null_indices:
|
269 |
-
random_response_times[idx] = None
|
270 |
-
|
271 |
-
data: Dict[str, Any] = {
|
272 |
-
'Timestamp': random_timestamps,
|
273 |
-
'ResponseTime(ms)': random_response_times
|
274 |
-
}
|
275 |
-
df: pd.DataFrame = pd.DataFrame(data)
|
276 |
-
return df
|
277 |
-
|
278 |
-
def calculate_percentile(
|
279 |
-
df: pd.DataFrame,
|
280 |
-
freq: str,
|
281 |
-
percentile: float
|
282 |
-
) -> pd.DataFrame:
|
283 |
-
percentile_df: pd.DataFrame = df.groupby(pd.Grouper(key='Timestamp', freq=freq))["ResponseTime(ms)"]\
|
284 |
-
.quantile(percentile).reset_index(name=f"p{int(percentile * 100)}_ResponseTime(ms)")
|
285 |
-
percentile_df.replace(to_replace=np.nan, value=None, inplace=True)
|
286 |
-
return percentile_df
|
287 |
-
|
288 |
-
def aggregate_data(
|
289 |
-
df: pd.DataFrame,
|
290 |
-
period_length: str
|
291 |
-
) -> pd.DataFrame:
|
292 |
-
aggregation_funcs = {
|
293 |
-
'p50': lambda x: np.percentile(x.dropna(), 50),
|
294 |
-
'p95': lambda x: np.percentile(x.dropna(), 95),
|
295 |
-
'p99': lambda x: np.percentile(x.dropna(), 99),
|
296 |
-
'max': lambda x: np.max(x.dropna()),
|
297 |
-
'min': lambda x: np.min(x.dropna()),
|
298 |
-
'average': lambda x: np.mean(x.dropna())
|
299 |
-
}
|
300 |
-
|
301 |
-
summary_df = df.groupby(pd.Grouper(key='Timestamp', freq=period_length)).agg(
|
302 |
-
p50=('ResponseTime(ms)', aggregation_funcs['p50']),
|
303 |
-
p95=('ResponseTime(ms)', aggregation_funcs['p95']),
|
304 |
-
p99=('ResponseTime(ms)', aggregation_funcs['p99']),
|
305 |
-
max=('ResponseTime(ms)', aggregation_funcs['max']),
|
306 |
-
min=('ResponseTime(ms)', aggregation_funcs['min']),
|
307 |
-
average=('ResponseTime(ms)', aggregation_funcs['average']),
|
308 |
-
).reset_index()
|
309 |
-
return summary_df
|
310 |
-
|
311 |
-
def chunk_list(input_list: List[Any], size: int = 3) -> Iterator[List[Any]]:
|
312 |
-
while input_list:
|
313 |
-
chunk: List[Any] = input_list[:size]
|
314 |
-
yield chunk
|
315 |
-
input_list = input_list[size:]
|
316 |
-
|
317 |
-
def evaluate_alarm_state(
|
318 |
-
summary_df: pd.DataFrame,
|
319 |
-
threshold: int,
|
320 |
-
datapoints_to_alarm: int,
|
321 |
-
evaluation_range: int,
|
322 |
-
aggregation_function: str,
|
323 |
-
alarm_condition: str
|
324 |
-
) -> pd.DataFrame:
|
325 |
-
data_points: List[Optional[float]] = list(summary_df[aggregation_function].values)
|
326 |
-
|
327 |
-
data_table_dict: Dict[str, List[Any]] = {
|
328 |
-
"DataPoints": [],
|
329 |
-
"# of data points that must be filled": [],
|
330 |
-
"MISSING": [],
|
331 |
-
"IGNORE": [],
|
332 |
-
"BREACHING": [],
|
333 |
-
"NOT BREACHING": []
|
334 |
-
}
|
335 |
-
|
336 |
-
def check_condition(value, threshold, condition):
|
337 |
-
if condition == '>':
|
338 |
-
return value > threshold
|
339 |
-
elif condition == '>=':
|
340 |
-
return value >= threshold
|
341 |
-
elif condition == '<':
|
342 |
-
return value < threshold
|
343 |
-
elif condition == '<=':
|
344 |
-
return value <= threshold
|
345 |
-
|
346 |
-
for chunk in chunk_list(input_list=data_points, size=evaluation_range):
|
347 |
-
data_point_repr: str = ''
|
348 |
-
num_dp_that_must_be_filled: int = 0
|
349 |
-
|
350 |
-
for dp in chunk:
|
351 |
-
if dp is None:
|
352 |
-
data_point_repr += '-'
|
353 |
-
elif check_condition(dp, threshold, alarm_condition):
|
354 |
-
data_point_repr += 'X'
|
355 |
-
else:
|
356 |
-
data_point_repr += '0'
|
357 |
-
|
358 |
-
if len(chunk) < evaluation_range:
|
359 |
-
data_point_repr += '-' * (evaluation_range - len(chunk))
|
360 |
-
|
361 |
-
if data_point_repr.count('-') > (evaluation_range - datapoints_to_alarm):
|
362 |
-
num_dp_that_must_be_filled = datapoints_to_alarm - sum([data_point_repr.count('0'), data_point_repr.count('X')])
|
363 |
-
|
364 |
-
data_table_dict["DataPoints"].append(data_point_repr)
|
365 |
-
data_table_dict["# of data points that must be filled"].append(num_dp_that_must_be_filled)
|
366 |
-
|
367 |
-
if num_dp_that_must_be_filled > 0:
|
368 |
-
data_table_dict["MISSING"].append("INSUFFICIENT_DATA" if data_point_repr.count('-') == evaluation_range else "Retain current state")
|
369 |
-
data_table_dict["IGNORE"].append("Retain current state")
|
370 |
-
data_table_dict["BREACHING"].append("ALARM")
|
371 |
-
data_table_dict["NOT BREACHING"].append("OK")
|
372 |
-
else:
|
373 |
-
data_table_dict["MISSING"].append("OK")
|
374 |
-
data_table_dict["IGNORE"].append("Retain current state")
|
375 |
-
data_table_dict["BREACHING"].append("ALARM" if 'X' * datapoints_to_alarm in data_point_repr else "OK")
|
376 |
-
data_table_dict["NOT BREACHING"].append("ALARM" if '0' * datapoints_to_alarm not in data_point_repr else "OK")
|
377 |
-
|
378 |
-
return pd.DataFrame(data_table_dict)
|
379 |
-
|
380 |
-
|
381 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils.py
CHANGED
@@ -31,7 +31,7 @@ def generate_random_data(
|
|
31 |
random_response_times[idx] = None
|
32 |
|
33 |
data: Dict[str, Any] = {
|
34 |
-
'
|
35 |
'ResponseTime(ms)': random_response_times
|
36 |
}
|
37 |
df: pd.DataFrame = pd.DataFrame(data)
|
@@ -42,7 +42,7 @@ def calculate_percentile(
|
|
42 |
freq: str,
|
43 |
percentile: float
|
44 |
) -> pd.DataFrame:
|
45 |
-
percentile_df: pd.DataFrame = df.groupby(pd.Grouper(key='
|
46 |
.quantile(percentile).reset_index(name=f"p{int(percentile * 100)}_ResponseTime(ms)")
|
47 |
percentile_df.replace(to_replace=np.nan, value=None, inplace=True)
|
48 |
return percentile_df
|
@@ -63,7 +63,7 @@ def aggregate_data(
|
|
63 |
'average': lambda x: np.mean(x.dropna()) if not x.dropna().empty else np.nan
|
64 |
}
|
65 |
|
66 |
-
summary_df = df.groupby(pd.Grouper(key='
|
67 |
p50=('ResponseTime(ms)', aggregation_funcs['p50']),
|
68 |
p95=('ResponseTime(ms)', aggregation_funcs['p95']),
|
69 |
p99=('ResponseTime(ms)', aggregation_funcs['p99']),
|
@@ -89,7 +89,7 @@ def re_aggregate_data(
|
|
89 |
'average': lambda x: np.mean(x.dropna()) if not x.dropna().empty else np.nan
|
90 |
}
|
91 |
|
92 |
-
summary_df = df.groupby(pd.Grouper(key='
|
93 |
p50=('p50', aggregation_funcs['p50']),
|
94 |
p95=('p95', aggregation_funcs['p95']),
|
95 |
p99=('p99', aggregation_funcs['p99']),
|
@@ -99,6 +99,26 @@ def re_aggregate_data(
|
|
99 |
).reset_index()
|
100 |
return summary_df
|
101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
|
103 |
def chunk_list(input_list: List[Any], size: int = 3) -> Iterator[List[Any]]:
|
104 |
while input_list:
|
|
|
31 |
random_response_times[idx] = None
|
32 |
|
33 |
data: Dict[str, Any] = {
|
34 |
+
'timestamp': random_timestamps,
|
35 |
'ResponseTime(ms)': random_response_times
|
36 |
}
|
37 |
df: pd.DataFrame = pd.DataFrame(data)
|
|
|
42 |
freq: str,
|
43 |
percentile: float
|
44 |
) -> pd.DataFrame:
|
45 |
+
percentile_df: pd.DataFrame = df.groupby(pd.Grouper(key='timestamp', freq=freq))["ResponseTime(ms)"]\
|
46 |
.quantile(percentile).reset_index(name=f"p{int(percentile * 100)}_ResponseTime(ms)")
|
47 |
percentile_df.replace(to_replace=np.nan, value=None, inplace=True)
|
48 |
return percentile_df
|
|
|
63 |
'average': lambda x: np.mean(x.dropna()) if not x.dropna().empty else np.nan
|
64 |
}
|
65 |
|
66 |
+
summary_df = df.groupby(pd.Grouper(key='timestamp', freq=period_length)).agg(
|
67 |
p50=('ResponseTime(ms)', aggregation_funcs['p50']),
|
68 |
p95=('ResponseTime(ms)', aggregation_funcs['p95']),
|
69 |
p99=('ResponseTime(ms)', aggregation_funcs['p99']),
|
|
|
89 |
'average': lambda x: np.mean(x.dropna()) if not x.dropna().empty else np.nan
|
90 |
}
|
91 |
|
92 |
+
summary_df = df.groupby(pd.Grouper(key='timestamp', freq=period_length)).agg(
|
93 |
p50=('p50', aggregation_funcs['p50']),
|
94 |
p95=('p95', aggregation_funcs['p95']),
|
95 |
p99=('p99', aggregation_funcs['p99']),
|
|
|
99 |
).reset_index()
|
100 |
return summary_df
|
101 |
|
102 |
+
def downsample(df, period_minutes):
|
103 |
+
# Create a new datetime index at specified intervals
|
104 |
+
freq_str = f'{period_minutes}T'
|
105 |
+
new_index = pd.date_range(start=df['timestamp'].min(), end=df['timestamp'].max(), freq=freq_str)
|
106 |
+
|
107 |
+
# Create an empty DataFrame with the new index
|
108 |
+
df_downsampled = pd.DataFrame(index=new_index)
|
109 |
+
|
110 |
+
# Set the original DataFrame's index to the timestamp column
|
111 |
+
df.set_index('timestamp', inplace=True)
|
112 |
+
|
113 |
+
# Interpolate the values for each column
|
114 |
+
for column in df.columns:
|
115 |
+
df_downsampled[column] = df[column].resample(freq_str).interpolate(method='linear')
|
116 |
+
|
117 |
+
# Reset index to have timestamp as a column again
|
118 |
+
df_downsampled.reset_index(inplace=True)
|
119 |
+
df_downsampled.rename(columns={'index': 'timestamp'}, inplace=True)
|
120 |
+
|
121 |
+
return df_downsampled
|
122 |
|
123 |
def chunk_list(input_list: List[Any], size: int = 3) -> Iterator[List[Any]]:
|
124 |
while input_list:
|