Skip to content

Commit 6eac036

Browse files
committed
feat(packages): Add random-alerts package
1 parent 0846acd commit 6eac036

File tree

8 files changed

+528
-0
lines changed

8 files changed

+528
-0
lines changed

packages/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
{
5858
lido-withdrawals-automation = pkgs.callPackage ./lido-withdrawals-automation { };
5959
pyroscope = pkgs.callPackage ./pyroscope { };
60+
random-alerts = pkgs.callPackage ./random-alerts { };
6061
}
6162
// optionalAttrs (system == "x86_64-linux" || system == "aarch64-darwin") {
6263
grafana-agent = import ./grafana-agent { inherit inputs'; };

packages/random-alerts/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
build

packages/random-alerts/default.nix

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{ buildDubPackage, ... }:
2+
buildDubPackage rec {
3+
pname = "random-alerts";
4+
version = "1.0.0";
5+
src = ./.;
6+
dubLock = {
7+
dependencies = { };
8+
};
9+
installPhase = ''
10+
mkdir -p $out/bin
11+
install -m755 ./build/${pname} $out/bin/${pname}
12+
'';
13+
meta.mainProgram = pname;
14+
}

packages/random-alerts/dub.sdl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name "random-alerts"
2+
3+
targetPath "build"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"fileVersion": 1,
3+
"versions": {
4+
}
5+
}

packages/random-alerts/src/main.d

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import core.thread : Thread;
2+
import std.datetime : Duration, Clock, seconds, TimeOfDay;
3+
import std.format : format;
4+
import std.getopt : getopt, getOptConfig = config;
5+
import std.json : JSONValue, parseJSON, JSONOptions;
6+
import std.logger : infof, errorf, tracef, LogLevel;
7+
import std.random : uniform;
8+
import std.exception : enforce;
9+
import core.time : minutes, seconds;
10+
11+
import utils.json : toJSON;
12+
13+
struct Params
14+
{
15+
Duration minWaitTime;
16+
Duration maxWaitTime;
17+
Duration alertDuration;
18+
string url;
19+
TimeOfDay startTime;
20+
TimeOfDay endTime;
21+
}
22+
23+
struct Alert
24+
{
25+
string[string] labels;
26+
Annotation annotations;
27+
string startsAt;
28+
string endsAt;
29+
string generatorURL;
30+
31+
struct Annotation
32+
{
33+
string alert_type;
34+
string title;
35+
string summary;
36+
}
37+
}
38+
39+
int main(string[] args)
40+
{
41+
LogLevel logLevel = LogLevel.info;
42+
string url;
43+
string startTime = "00:00:00";
44+
string endTime = "23:59:59";
45+
uint minWaitTimeInSeconds = 3600; // 1 hour
46+
uint maxWaitTimeInSeconds = 14400; // 4 hours
47+
uint alertDurationInSeconds = 3600; // 1 hour
48+
49+
try
50+
{
51+
args.getopt(
52+
getOptConfig.required, "url", &url,
53+
"start-time", &startTime,
54+
"end-time", &endTime,
55+
"min-wait-time", &minWaitTimeInSeconds,
56+
"max-wait-time", &maxWaitTimeInSeconds,
57+
"alert-duration", &alertDurationInSeconds,
58+
"log-level", &logLevel,
59+
);
60+
61+
enforce(minWaitTimeInSeconds <= maxWaitTimeInSeconds, "Make sure that `max-wait-time` is greater than `min-wait-time`.");
62+
63+
setLogLevel(logLevel);
64+
65+
executeAtRandomIntervals(
66+
Params(
67+
url: url,
68+
startTime: TimeOfDay.fromISOExtString(startTime),
69+
endTime: TimeOfDay.fromISOExtString(endTime),
70+
minWaitTime: minWaitTimeInSeconds.seconds(),
71+
maxWaitTime: maxWaitTimeInSeconds.seconds(),
72+
alertDuration: alertDurationInSeconds.seconds(),
73+
)
74+
);
75+
76+
}
77+
catch (Exception e)
78+
{
79+
errorf("Exception: %s", e.message);
80+
return 1;
81+
}
82+
return 0;
83+
}
84+
85+
auto getRandomDuration(Duration min, Duration max) =>
86+
uniform(min.total!"seconds", max.total!"seconds").seconds;
87+
88+
void executeAtRandomIntervals(Params params)
89+
{
90+
with(params) while (true)
91+
{
92+
auto currentTime = Clock.currTime();
93+
auto currentTimeTOD = cast(TimeOfDay)currentTime;
94+
Duration randomDuration = getRandomDuration(minWaitTime, maxWaitTime);
95+
auto randomTime = currentTime + randomDuration;
96+
97+
tracef("The operating time is: [%s .. %s]", startTime, endTime);
98+
tracef("The next alarm will be activated in %s", randomTime);
99+
100+
Thread.sleep(randomDuration); // sleep till the request is ready to be posted.
101+
102+
if (currentTimeTOD >= startTime && currentTimeTOD <= endTime)
103+
{
104+
infof("Posting alert... ");
105+
postAlert(url, alertDuration);
106+
infof("Alert posted successfully.");
107+
}
108+
else
109+
{
110+
infof("This service is outside working hours. The operating time is: [%s .. %s]", startTime, endTime);
111+
}
112+
113+
Duration remainingTime = maxWaitTime - randomDuration;
114+
115+
tracef("The current interval's end will be at %s", (currentTime + remainingTime));
116+
Thread.sleep(remainingTime); // sleep till the cycle is over.
117+
}
118+
}
119+
120+
void postAlert(string alertManagerEndpoint, Duration alertDuration)
121+
{
122+
string url = alertManagerEndpoint ~ "/api/v2/alerts";
123+
124+
postJson(url, [
125+
Alert(
126+
startsAt: Clock.currTime.toUTC.toISOExtString(0),
127+
endsAt: (Clock.currTime + alertDuration).toUTC.toISOExtString(0),
128+
generatorURL: "http://localhost:9090",
129+
labels: [
130+
"alertname": "Random alert",
131+
"severity": "critical",
132+
"environment": "staging",
133+
"job": "test-monitoring",
134+
],
135+
annotations: Alert.Annotation(
136+
alert_type: "critical",
137+
title: "Write report",
138+
summary: "The alert was triggered at '%s'".format(Clock.currTime.toUTC)
139+
)
140+
)
141+
]);
142+
}
143+
144+
JSONValue postJson(T)(string url, in T value)
145+
{
146+
import std.net.curl : HTTP, post, HTTPStatusException;
147+
148+
auto jsonRequest = value
149+
.toJSON
150+
.toPrettyString(JSONOptions.doNotEscapeSlashes);
151+
152+
tracef("Sending request to '%s':\n%s", url, jsonRequest);
153+
154+
auto http = HTTP();
155+
http.addRequestHeader("Content-Type", "application/json");
156+
157+
auto response = post(url, jsonRequest, http);
158+
return response.parseJSON;
159+
}
160+
161+
void setLogLevel(LogLevel l)
162+
{
163+
import std.logger : globalLogLevel, sharedLog;
164+
globalLogLevel = l;
165+
(cast()sharedLog()).logLevel = l;
166+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
module utils.json;
2+
3+
import std.traits: isNumeric, isArray, isSomeChar, ForeachType, isBoolean, isAssociativeArray;
4+
import std.json: parseJSON, JSONValue, JSONOptions, JSONType;
5+
import std.conv: to;
6+
import std.string: strip;
7+
import std.range: front;
8+
import std.algorithm: map;
9+
import std.array: join, array, replace, split;
10+
import std.datetime: SysTime;
11+
import std.sumtype: SumType, isSumType;
12+
import core.stdc.string: strlen;
13+
14+
import utils.string;
15+
16+
JSONValue toJSON(T)(in T value, bool simplify = false)
17+
{
18+
static if (is(T == enum))
19+
{
20+
return JSONValue(value.enumToString);
21+
}
22+
else static if (is(T == bool) || is(T == string) || isSomeChar!T || isNumeric!T)
23+
return JSONValue(value);
24+
else static if ((isArray!T && isSomeChar!(ForeachType!T)) ) {
25+
return JSONValue(value.idup[0..(strlen(value.ptr)-1)]);
26+
}
27+
else static if (isArray!T)
28+
{
29+
if (simplify && value.length == 1)
30+
return value.front.toJSON(simplify);
31+
else if (simplify && isBoolean!(ForeachType!T) ) {
32+
static if (isBoolean!(ForeachType!T)) {
33+
return JSONValue((value.map!(a => a ? '1' : '0').array).to!string);
34+
}
35+
else {assert(0);}
36+
}
37+
else {
38+
JSONValue[] result;
39+
foreach (elem; value)
40+
result ~= elem.toJSON(simplify);
41+
return JSONValue(result);
42+
}
43+
}
44+
else static if (is(T == SysTime)) {
45+
return JSONValue(value.toISOExtString());
46+
}
47+
else static if (is(T == struct))
48+
{
49+
JSONValue[string] result;
50+
auto name = "";
51+
static foreach (idx, field; T.tupleof)
52+
{
53+
name = __traits(identifier, field).strip("_");
54+
result[name] = value.tupleof[idx].toJSON(simplify);
55+
}
56+
return JSONValue(result);
57+
}
58+
else static if (is(T == K[V], K, V))
59+
{
60+
JSONValue[string] result;
61+
foreach (key, field; value)
62+
{
63+
result[key] = field.toJSON(simplify);
64+
}
65+
return JSONValue(result);
66+
}
67+
else
68+
static assert(false, "Unsupported type: `" ~ __traits(identifier, T) ~ "`");
69+
}
70+
71+
version(unittest)
72+
{
73+
enum TestEnum
74+
{
75+
@StringRepresentation("supercalifragilisticexpialidocious")
76+
a,
77+
b,
78+
c
79+
}
80+
struct TestStruct
81+
{
82+
int a;
83+
string b;
84+
bool c;
85+
}
86+
struct TestStruct2
87+
{
88+
int a;
89+
TestStruct b;
90+
}
91+
struct TestStruct3
92+
{
93+
int a;
94+
TestStruct2 b;
95+
}
96+
}
97+
98+
@("toJSON")
99+
unittest
100+
{
101+
assert(1.toJSON == JSONValue(1));
102+
assert(true.toJSON == JSONValue(true));
103+
assert("test".toJSON == JSONValue("test"));
104+
assert([1, 2, 3].toJSON == JSONValue([1, 2, 3]));
105+
assert(["a", "b", "c"].toJSON == JSONValue(["a", "b", "c"]));
106+
assert([TestEnum.a, TestEnum.b, TestEnum.c].toJSON == JSONValue(["supercalifragilisticexpialidocious", "b", "c"]));
107+
TestStruct testStruct = { 1, "test", true };
108+
assert(testStruct.toJSON == JSONValue(["a": JSONValue(1), "b": JSONValue("test"), "c": JSONValue(true)]));
109+
TestStruct2 testStruct2 = { 1, testStruct };
110+
assert(testStruct2.toJSON == JSONValue(["a": JSONValue(1), "b": JSONValue(["a": JSONValue(1), "b": JSONValue("test"), "c": JSONValue(true)])]));
111+
TestStruct3 testStruct3 = { 1, testStruct2 };
112+
assert(testStruct3.toJSON == JSONValue(["a": JSONValue(1), "b": JSONValue(["a": JSONValue(1), "b": JSONValue(["a": JSONValue(1), "b": JSONValue("test"), "c": JSONValue(true)])])]));
113+
}

0 commit comments

Comments
 (0)