-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathBaseMonitor.cs
173 lines (158 loc) · 6.29 KB
/
BaseMonitor.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Metrist.Core
{
/// A simple logger function. Depending on how a monitor is run, full logging
/// may not be available. Therefore, we pass this delegate around; the monitor
/// running is responsible for making sure that it emits logging in a useful
/// place.
public delegate void Logger(string message);
/// Simple function that can be used to ask for a wait on a webhook from
/// an external processing source
public delegate string WaitForWebhook(string uid);
/// Monitor configuration. Every monitor is associated with a subclass
/// of this base class, adding monitor-specific configuration (like API keys
/// to use, etcetera.
public class BaseMonitorConfig
{
/// Identifier for the monitor for logging purposes etc. Usually all
/// lowercased.
public string MonitorId { get; set; }
/// Where the monitor is running; a region or host name.
public string InstanceIdentifier { get; set; }
/// Whether a cleanup should be run to make sure that unneeded
/// data is removed from the monitored API
public bool IsCleanupEnabled { get; set; }
public BaseMonitorConfig() {}
public BaseMonitorConfig(BaseMonitorConfig src)
{
MonitorId = src.MonitorId;
InstanceIdentifier = src.InstanceIdentifier;
IsCleanupEnabled = src.IsCleanupEnabled;
}
}
/// Base class for all monitors. This class has a bunch of support code which, including
/// the "Scenario" DSL, makes writing monitors easy.
public abstract class BaseMonitor
{
public const string TRAP_ERROR_STRING = "METRIST_MONITOR_ERROR";
private readonly BaseMonitorConfig _config;
public BaseMonitor(BaseMonitorConfig config)
{
_config = config;
}
/// Time an action, return the elapsed time
public static double Timed(Action a)
{
var (time, dummy) = Timed(() =>
{
a();
return -1;
});
return time;
}
/// Time the duration of a task that the <c>taskMaker</c> argument
/// passes in.
public static double Timed(Func<Task> taskMaker)
{
return Timed(() =>
{
Task t = taskMaker();
t.Wait();
});
}
/// Time the duration of a task that the <c>taskMaker</c> argument
/// passes in and return the result of the task.
public static (double, T) Timed<T>(Func<Task<T>> taskMaker)
{
return Timed(() =>
{
var t = taskMaker();
t.Wait();
return t.Result;
});
}
/// Time the duration of the function and return the result
/// of calling the function.
public static (double, T) Timed<T>(Func<T> a)
{
var watch = new Stopwatch();
watch.Start();
T result = a();
watch.Stop();
return (watch.ElapsedMilliseconds, result);
}
/// Wait on a task and return its result.
public static T WaitWithResult<T>(Task<T> t)
{
t.Wait();
return t.Result;
}
/// Utility for functions that deal with webhooks
public double TimeWebhook(WaitForWebhook waiter, string dedupKey, DateTime startTime, Action<JObject> handler)
{
var response = waiter(dedupKey);
var responseObj = JsonConvert.DeserializeObject<JObject>(response);
var inserted = DateTime.Parse(responseObj["inserted_at"].Value<string>());
var data = responseObj["data"].Value<string>();
var obj = JsonConvert.DeserializeObject<JObject>(data);
handler(obj);
return (inserted - startTime).TotalMilliseconds;
}
public double TimeWebhook(WaitForWebhook waiter, string dedupKey, DateTime startTime)
{
return TimeWebhook(waiter, dedupKey, startTime, _ => {});
}
/// Time an action, but on certain exceptions retry. This can be used to
/// retry when data isn't found (404 errors) because of propagation delays
/// between API instances we're testing.
/// There's no maximum time here - timeouts are configured in the backend and
/// enforced by the orchestrator, so the monitor will get killed if this takes
/// too long.
public static double TimedWithRetries(Action a, Func<Exception, bool> shouldRetry)
{
return TimedWithRetries(a, shouldRetry, _ => { });
}
public static double TimedWithRetries(Func<Task> a, Func<Exception, bool> shouldRetry, Logger logger, int sleepTimeout = 1000)
{
return DoTimedWithRetries(() => Timed(a), shouldRetry, logger, sleepTimeout);
}
public static double TimedWithRetries(Action a, Func<Exception, bool> shouldRetry, Logger logger, int sleepTimeout = 1000)
{
return DoTimedWithRetries(() => Timed(a), shouldRetry, logger, sleepTimeout);
}
private static double DoTimedWithRetries(Func<double> a, Func<Exception, bool> shouldRetry, Logger logger, int sleepTimeout = 1000)
{
int retry = 0;
while (true)
{
try
{
logger($"Attempting retry {retry}");
var time = a();
logger($"Successful in {time} ms, returning");
return time;
}
catch (Exception ex)
{
logger($"{TRAP_ERROR_STRING} - Exception caught: {ex.Message}");
if (shouldRetry(ex))
{
retry++;
logger("Should retry, sleeping");
Thread.Sleep(sleepTimeout);
}
else
{
logger("Not something we should retry on, rethrowing exception");
throw;
}
}
}
}
}
}