@certik/skynet 0.7.20 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/app.js +476 -0
- package/cli.js +51 -0
- package/deploy.js +188 -143
- package/examples/consumer +53 -0
- package/examples/indexer +39 -8
- package/examples/{deploy-consumer → legacy-deploy-consumer} +2 -2
- package/examples/{deploy-indexer → legacy-deploy-indexer} +4 -3
- package/examples/{deploy-mode-indexer → legacy-deploy-mode-indexer} +2 -2
- package/examples/{deploy-producer → legacy-deploy-producer} +2 -2
- package/examples/legacy-indexer +25 -0
- package/examples/{kafka-consumer → legacy-kafka-consumer} +1 -1
- package/examples/{kafka-producer → legacy-kafka-producer} +2 -2
- package/examples/legacy-mode-indexer +64 -0
- package/examples/mode-indexer +51 -38
- package/examples/producer +68 -0
- package/indexer.js +10 -7
- package/kafka.js +9 -7
- package/monitor.js +246 -0
- package/package.json +1 -1
- package/selector.js +5 -5
- package/slack.js +47 -31
package/monitor.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
const meow = require("meow");
|
|
2
|
+
const fetch = require("node-fetch");
|
|
3
|
+
const { getSelectorFlags, getSelectorDesc, toSelectorString } = require("./selector");
|
|
4
|
+
const { getBinaryName } = require("./cli");
|
|
5
|
+
const { exponentialRetry } = require("./availability");
|
|
6
|
+
const { getIndexerLatestId, getIndexerValidatedId, getIndexerState } = require("./indexer");
|
|
7
|
+
const { postMessage } = require("./slack");
|
|
8
|
+
const { getJobName } = require("./deploy");
|
|
9
|
+
|
|
10
|
+
const ERROR_LEVEL = {
|
|
11
|
+
INFO: "Info",
|
|
12
|
+
CRITICAL: "Critical",
|
|
13
|
+
WARNING: "Warning",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const LEVEL_EMOJI = {
|
|
17
|
+
Critical: ":exclamation:",
|
|
18
|
+
Warning: ":warning:",
|
|
19
|
+
Info: "",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const LEVEL_PRIORITY = {
|
|
23
|
+
Critical: 3,
|
|
24
|
+
Warning: 2,
|
|
25
|
+
Info: 1,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function sortErrors(errors) {
|
|
29
|
+
return errors.sort((e1, e2) => (LEVEL_PRIORITY[e2.type] || 0) - (LEVEL_PRIORITY[e1.type] || 0));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getNomadUrl() {
|
|
33
|
+
return process.env.SKYNET_NOMAD_URL || "http://localhost:4646";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function checkMostRecentAllocationStatus(name) {
|
|
37
|
+
const jobsRes = await fetch(`http://localhost:4646/v1/jobs?prefix=${name}`);
|
|
38
|
+
|
|
39
|
+
if (!jobsRes.ok) {
|
|
40
|
+
console.log(`request local nomad API failed`);
|
|
41
|
+
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const jobs = await jobsRes.json();
|
|
46
|
+
|
|
47
|
+
if (jobs.length === 0) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const mostRecentJob = jobs[jobs.length - 1];
|
|
52
|
+
|
|
53
|
+
const allocationsRes = await fetch(`http://localhost:4646/v1/job/${mostRecentJob.ID}/allocations`);
|
|
54
|
+
|
|
55
|
+
if (!allocationsRes.ok) {
|
|
56
|
+
console.log(`request local nomad API failed`);
|
|
57
|
+
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const allocations = await allocationsRes.json();
|
|
62
|
+
|
|
63
|
+
if (allocations.length === 0) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const mostRecentAllocation = allocations[allocations.length - 1];
|
|
68
|
+
|
|
69
|
+
const tasks = Object.keys(mostRecentAllocation.TaskStates);
|
|
70
|
+
|
|
71
|
+
for (let task of tasks) {
|
|
72
|
+
if (mostRecentAllocation.TaskStates[task].Failed) {
|
|
73
|
+
// TODO if we could include a link to the failed allocation
|
|
74
|
+
// which could be very useful
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
type: ERROR_LEVEL.CRITICAL,
|
|
78
|
+
message: `Job ${name}'s most recent allocation failed, please investigate`,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createMonitor({
|
|
88
|
+
binaryName,
|
|
89
|
+
name,
|
|
90
|
+
type = "stateless",
|
|
91
|
+
mode = false,
|
|
92
|
+
slackChannel,
|
|
93
|
+
selector = {},
|
|
94
|
+
check,
|
|
95
|
+
maxRetry = 2,
|
|
96
|
+
}) {
|
|
97
|
+
function monitor() {
|
|
98
|
+
if (!binaryName) {
|
|
99
|
+
binaryName = getBinaryName();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const cli = meow(
|
|
103
|
+
`
|
|
104
|
+
Usage
|
|
105
|
+
$ ${binaryName} <options>
|
|
106
|
+
|
|
107
|
+
Options
|
|
108
|
+
${
|
|
109
|
+
mode ? " --mode could be delta/rebuild/resume-rebuild/validate/one/range/reset\n" : ""
|
|
110
|
+
}${getSelectorDesc(selector)}
|
|
111
|
+
--verbose Output debug messages
|
|
112
|
+
`,
|
|
113
|
+
{
|
|
114
|
+
description: false,
|
|
115
|
+
version: false,
|
|
116
|
+
flags: {
|
|
117
|
+
...getSelectorFlags(selector),
|
|
118
|
+
...(mode && {
|
|
119
|
+
mode: {
|
|
120
|
+
type: "string",
|
|
121
|
+
default: "delta",
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
verbose: {
|
|
125
|
+
type: "boolean",
|
|
126
|
+
default: false,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
async function runCheck({ verbose, mode, ...selectorFlags }) {
|
|
133
|
+
const startTime = Date.now();
|
|
134
|
+
console.log(`[MONITOR] starting check, ${toSelectorString(selectorFlags, ", ")}`);
|
|
135
|
+
|
|
136
|
+
const state = {};
|
|
137
|
+
|
|
138
|
+
if (type === "stateful") {
|
|
139
|
+
state.latestId = await getIndexerLatestId(name, selectorFlags);
|
|
140
|
+
state.validatedId = await getIndexerValidatedId(name, selectorFlags);
|
|
141
|
+
state.buildState = await getIndexerState(name, selectorFlags);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let result = await exponentialRetry(
|
|
145
|
+
async () => {
|
|
146
|
+
try {
|
|
147
|
+
const errors = await check({ verbose, state, mode, ...selectorFlags });
|
|
148
|
+
|
|
149
|
+
if (!Array.isArray(errors)) {
|
|
150
|
+
throw new Error(`check function must return array of error messages`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return errors;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.log(`[MONITOR] got error in check`, err);
|
|
156
|
+
|
|
157
|
+
return [`${err.message}`];
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
maxRetry,
|
|
162
|
+
initialDuration: 10000,
|
|
163
|
+
growFactor: 3,
|
|
164
|
+
test: (r) => r.length === 0,
|
|
165
|
+
verbose,
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const allocationErrors = await checkMostRecentAllocationStatus(name);
|
|
170
|
+
|
|
171
|
+
result = result.concat(allocationErrors);
|
|
172
|
+
|
|
173
|
+
if (result.length > 0) {
|
|
174
|
+
console.log("Found Errors", result);
|
|
175
|
+
|
|
176
|
+
if (slackChannel) {
|
|
177
|
+
await postMessage(slackChannel, {
|
|
178
|
+
text: `[Monitor] ${name} Job Errors: ${result.join("\n")}`,
|
|
179
|
+
blocks: [
|
|
180
|
+
{
|
|
181
|
+
type: "header",
|
|
182
|
+
text: {
|
|
183
|
+
type: "plain_text",
|
|
184
|
+
text: `${name} Monitor Errors`,
|
|
185
|
+
emoji: true,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
type: "divider",
|
|
190
|
+
},
|
|
191
|
+
...sortErrors(result)
|
|
192
|
+
.map((m) => {
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
type: "context",
|
|
196
|
+
elements: [
|
|
197
|
+
{
|
|
198
|
+
type: "plain_text",
|
|
199
|
+
text: `${LEVEL_EMOJI[m.type || ERROR_LEVEL.CRITICAL]} ${m.type || ERROR_LEVEL.CRITICAL}`,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
type: "section",
|
|
205
|
+
text: {
|
|
206
|
+
type: "mrkdwn",
|
|
207
|
+
text: m.message || m,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
})
|
|
212
|
+
.flat(),
|
|
213
|
+
{
|
|
214
|
+
type: "actions",
|
|
215
|
+
elements: [
|
|
216
|
+
{
|
|
217
|
+
type: "button",
|
|
218
|
+
text: {
|
|
219
|
+
type: "plain_text",
|
|
220
|
+
text: "View Details",
|
|
221
|
+
},
|
|
222
|
+
value: "view_details",
|
|
223
|
+
url: `${getNomadUrl()}/ui/jobs/${getJobName(name, selectorFlags, mode)}`,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
throw new Error(`[MONITOR] failed due to critical errors`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(`[MONITOR] check successfully in ${Date.now() - startTime}ms`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
runCheck(cli.flags).catch((err) => {
|
|
238
|
+
console.error(err);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { monitor };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = { createMonitor, ERROR_LEVEL };
|
package/package.json
CHANGED
package/selector.js
CHANGED
|
@@ -9,9 +9,9 @@ function getSpaces(num) {
|
|
|
9
9
|
|
|
10
10
|
function getSelectorDesc(selector) {
|
|
11
11
|
return Object.keys(selector)
|
|
12
|
-
.map(name => {
|
|
12
|
+
.map((name) => {
|
|
13
13
|
return ` --${name}${getSpaces(14 - name.length)}${
|
|
14
|
-
selector[name].desc
|
|
14
|
+
selector[name].desc || selector[name].description || ""
|
|
15
15
|
}`;
|
|
16
16
|
})
|
|
17
17
|
.join("\n");
|
|
@@ -21,7 +21,7 @@ function getSelectorFlags(selector) {
|
|
|
21
21
|
return Object.keys(selector).reduce((acc, name) => {
|
|
22
22
|
const flag = {
|
|
23
23
|
type: selector[name].type || "string",
|
|
24
|
-
isRequired: true
|
|
24
|
+
isRequired: true,
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
if (selector[name].default) {
|
|
@@ -40,7 +40,7 @@ function getSelectorFlags(selector) {
|
|
|
40
40
|
function toSelectorString(selectorFlags, delim = ",") {
|
|
41
41
|
return Object.keys(selectorFlags)
|
|
42
42
|
.sort() // deterministic
|
|
43
|
-
.map(flag => {
|
|
43
|
+
.map((flag) => {
|
|
44
44
|
return `${flag}=${selectorFlags[flag]}`;
|
|
45
45
|
})
|
|
46
46
|
.join(delim);
|
|
@@ -49,5 +49,5 @@ function toSelectorString(selectorFlags, delim = ",") {
|
|
|
49
49
|
module.exports = {
|
|
50
50
|
getSelectorDesc,
|
|
51
51
|
getSelectorFlags,
|
|
52
|
-
toSelectorString
|
|
52
|
+
toSelectorString,
|
|
53
53
|
};
|
package/slack.js
CHANGED
|
@@ -1,46 +1,62 @@
|
|
|
1
|
-
const { WebClient
|
|
1
|
+
const { WebClient } = require("@slack/web-api");
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function getToken() {
|
|
4
|
+
return process.env.SKYNET_SLACK_TOKEN;
|
|
5
|
+
}
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
logLevel: LogLevel.DEBUG,
|
|
8
|
-
});
|
|
7
|
+
function getClient() {
|
|
8
|
+
const token = getToken();
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
if (!token) {
|
|
11
|
+
throw new Error("Cannot communicate with slack due to missing slack token: SKYNET_SLACK_TOKEN");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const client = new WebClient(token);
|
|
15
|
+
|
|
16
|
+
return client;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function findConversation(client, name) {
|
|
20
|
+
const { conversations } = client;
|
|
21
|
+
|
|
22
|
+
// Call the conversations.list method using the built-in WebClient
|
|
23
|
+
let result = await conversations.list({
|
|
24
|
+
limit: 1000,
|
|
25
|
+
});
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
for (const channel of result.channels) {
|
|
28
|
+
if (channel.name === name) {
|
|
29
|
+
const conversationId = channel.id;
|
|
21
30
|
|
|
22
|
-
|
|
23
|
-
return conversationId;
|
|
24
|
-
}
|
|
31
|
+
return conversationId;
|
|
25
32
|
}
|
|
26
|
-
} catch (error) {
|
|
27
|
-
console.error(error);
|
|
28
33
|
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
async function
|
|
38
|
+
async function postMessage(channel, message) {
|
|
32
39
|
try {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
text: text,
|
|
37
|
-
// You could also use a blocks[] array to send richer content
|
|
38
|
-
});
|
|
40
|
+
const client = getClient();
|
|
41
|
+
|
|
42
|
+
const conversationId = await findConversation(client, channel);
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
let post = {};
|
|
45
|
+
|
|
46
|
+
if (typeof message === "string") {
|
|
47
|
+
post.text = message;
|
|
48
|
+
} else {
|
|
49
|
+
post = message;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await client.chat.postMessage({
|
|
53
|
+
channel: conversationId,
|
|
54
|
+
...post,
|
|
55
|
+
});
|
|
41
56
|
} catch (error) {
|
|
42
|
-
|
|
57
|
+
// no blocking
|
|
58
|
+
console.error("failed to post slack message", error);
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
module.exports = {
|
|
62
|
+
module.exports = { postMessage };
|