@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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@certik/skynet",
3
- "version": "0.7.20",
3
+ "version": "0.8.0",
4
4
  "description": "Skynet Shared JS library",
5
5
  "main": "index.js",
6
6
  "author": "CertiK Engineering",
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, LogLevel } = require("@slack/web-api");
1
+ const { WebClient } = require("@slack/web-api");
2
2
 
3
- const token = process.env.SLACK_TOKEN;
3
+ function getToken() {
4
+ return process.env.SKYNET_SLACK_TOKEN;
5
+ }
4
6
 
5
- // WebClient insantiates a client that can call API methods
6
- const client = new WebClient(token, {
7
- logLevel: LogLevel.DEBUG,
8
- });
7
+ function getClient() {
8
+ const token = getToken();
9
9
 
10
- async function findConversation(name) {
11
- try {
12
- // Call the conversations.list method using the built-in WebClient
13
- const result = await client.conversations.list({
14
- token: token,
15
- limit: 1000,
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
- for (const channel of result.channels) {
19
- if (channel.name === name) {
20
- const conversationId = channel.id;
27
+ for (const channel of result.channels) {
28
+ if (channel.name === name) {
29
+ const conversationId = channel.id;
21
30
 
22
- console.log("Found conversation ID: " + conversationId);
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 publishMessage(id, text) {
38
+ async function postMessage(channel, message) {
32
39
  try {
33
- const result = await client.chat.postMessage({
34
- token: token,
35
- channel: id,
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
- console.log(result);
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
- console.error(error);
57
+ // no blocking
58
+ console.error("failed to post slack message", error);
43
59
  }
44
60
  }
45
61
 
46
- module.exports = { publishMessage, findConversation };
62
+ module.exports = { postMessage };