Skip to content

Commit 5ac0b0e

Browse files
Merge pull request #176 from wttech/hc-run-script
Execution health check
2 parents d93b76e + 7756645 commit 5ac0b0e

File tree

8 files changed

+216
-26
lines changed

8 files changed

+216
-26
lines changed

core/src/main/java/dev/vml/es/acm/core/instance/HealthChecker.java

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package dev.vml.es.acm.core.instance;
22

3+
import dev.vml.es.acm.core.code.*;
4+
import dev.vml.es.acm.core.code.script.ExtensionScriptSyntax;
35
import dev.vml.es.acm.core.osgi.*;
46
import dev.vml.es.acm.core.repo.Repo;
7+
import dev.vml.es.acm.core.util.ExceptionUtils;
58
import dev.vml.es.acm.core.util.ResourceUtils;
69
import java.util.*;
710
import java.util.stream.Collectors;
811
import org.apache.commons.collections.CollectionUtils;
912
import org.apache.commons.io.FilenameUtils;
1013
import org.apache.commons.lang3.ArrayUtils;
14+
import org.apache.commons.lang3.StringUtils;
1115
import org.apache.sling.api.resource.ResourceResolver;
1216
import org.apache.sling.api.resource.ResourceResolverFactory;
1317
import org.apache.sling.discovery.DiscoveryService;
@@ -38,6 +42,9 @@ public class HealthChecker implements EventHandler {
3842
@Reference
3943
private ResourceResolverFactory resourceResolverFactory;
4044

45+
@Reference
46+
private Executor executor;
47+
4148
@Reference
4249
private InstanceInfo instanceInfo;
4350

@@ -85,6 +92,7 @@ private HealthStatus checkStatus(ResourceResolver resourceResolver) {
8592
checkBundles(result);
8693
checkEvents(result);
8794
checkComponents(result);
95+
checkCodeExecutor(result, resourceResolver);
8896
result.healthy = CollectionUtils.isEmpty(result.issues);
8997
return result;
9098
}
@@ -94,7 +102,8 @@ private void checkCluster(HealthStatus result) {
94102
return;
95103
}
96104
if (!isClusterLeader()) {
97-
result.issues.add(new HealthIssue(HealthIssueSeverity.CRITICAL, "Instance is not a cluster leader"));
105+
result.issues.add(new HealthIssue(
106+
HealthIssueSeverity.CRITICAL, HealthIssueCategory.INSTANCE, "Not a cluster leader", null));
98107
}
99108
}
100109

@@ -115,12 +124,16 @@ private void checkInstaller(HealthStatus result, ResourceResolver resourceResolv
115124
if (state.isActive()) {
116125
result.issues.add(new HealthIssue(
117126
HealthIssueSeverity.CRITICAL,
118-
String.format("Sling Installer is active (%d)", state.getActiveResourceCount())));
127+
HealthIssueCategory.INSTALLER,
128+
String.format("Active resource count: %d", state.getActiveResourceCount()),
129+
null));
119130
}
120131
if (state.isPaused()) {
121132
result.issues.add(new HealthIssue(
122133
HealthIssueSeverity.CRITICAL,
123-
String.format("Sling Installer is paused (%d)", state.getPauseCount())));
134+
HealthIssueCategory.INSTALLER,
135+
String.format("Pause count: %d", state.getPauseCount()),
136+
null));
124137
}
125138
}
126139

@@ -130,14 +143,20 @@ private void checkRepository(HealthStatus result, ResourceResolver resourceResol
130143
}
131144
Repo repo = new Repo(resourceResolver);
132145
if ((instanceInfo.getType() == InstanceType.CLOUD_CONTAINER) && !repo.isCompositeNodeStore()) {
133-
result.issues.add(
134-
new HealthIssue(HealthIssueSeverity.CRITICAL, "Repository is not yet using composite node store"));
146+
result.issues.add(new HealthIssue(
147+
HealthIssueSeverity.CRITICAL,
148+
HealthIssueCategory.REPOSITORY,
149+
"Composite node store not available",
150+
null));
135151
}
136152
if (ArrayUtils.isNotEmpty(config.repositoryPathsExisted())) {
137153
Arrays.stream(config.repositoryPathsExisted()).forEach(path -> {
138154
if (!repo.get(path).exists()) {
139155
result.issues.add(new HealthIssue(
140-
HealthIssueSeverity.CRITICAL, String.format("Repository path '%s' does not exist", path)));
156+
HealthIssueSeverity.CRITICAL,
157+
HealthIssueCategory.REPOSITORY,
158+
String.format("Path does not exist: '%s'", path),
159+
null));
141160
}
142161
});
143162
}
@@ -152,13 +171,17 @@ private void checkBundles(HealthStatus result) {
152171
if (!osgiScanner.isBundleResolved(bundle)) {
153172
result.issues.add(new HealthIssue(
154173
HealthIssueSeverity.CRITICAL,
155-
String.format("Bundle fragment '%s' is not resolved", bundle.getSymbolicName())));
174+
HealthIssueCategory.OSGI,
175+
String.format("Bundle fragment not resolved: '%s'", bundle.getSymbolicName()),
176+
null));
156177
}
157178
} else {
158179
if (!osgiScanner.isBundleActive(bundle)) {
159180
result.issues.add(new HealthIssue(
160181
HealthIssueSeverity.CRITICAL,
161-
String.format("Bundle '%s' is not active", bundle.getSymbolicName())));
182+
HealthIssueCategory.OSGI,
183+
String.format("Bundle not active: '%s'", bundle.getSymbolicName()),
184+
null));
162185
}
163186
}
164187
});
@@ -186,7 +209,10 @@ private void checkEvents(HealthStatus result) {
186209
Map<String, Long> eventCounts =
187210
recentEvents.stream().collect(Collectors.groupingBy(OsgiEvent::getTopic, Collectors.counting()));
188211
eventCounts.forEach((topic, count) -> result.issues.add(new HealthIssue(
189-
HealthIssueSeverity.CRITICAL, String.format("Event '%s' occurred (%d)", topic, count))));
212+
HealthIssueSeverity.CRITICAL,
213+
HealthIssueCategory.OSGI,
214+
String.format("Event occurred (%d): %s", count, topic),
215+
null)));
190216
}
191217
}
192218

@@ -213,6 +239,30 @@ private void checkComponents(HealthStatus result) {
213239
// TODO ...
214240
}
215241

242+
private void checkCodeExecutor(HealthStatus result, ResourceResolver resourceResolver) {
243+
try (ExecutionContext context = executor.createContext(
244+
ExecutionId.generate(), ExecutionMode.RUN, Code.consoleMinimal(), resourceResolver)) {
245+
context.setHistory(false);
246+
Execution execution = executor.execute(context);
247+
if (execution.getStatus() != ExecutionStatus.SUCCEEDED) {
248+
result.issues.add(new HealthIssue(
249+
HealthIssueSeverity.CRITICAL,
250+
HealthIssueCategory.CODE_EXECUTOR,
251+
String.format(
252+
"Execution not succeeded: %s",
253+
execution.getStatus().name().toLowerCase()),
254+
execution.getError()));
255+
}
256+
} catch (Exception e) {
257+
String error = ExceptionUtils.toString(e);
258+
String issue = StringUtils.contains(error, ExtensionScriptSyntax.MAIN_CLASS + ":")
259+
? "Extension script error"
260+
: "Execution context error";
261+
result.issues.add(
262+
new HealthIssue(HealthIssueSeverity.CRITICAL, HealthIssueCategory.CODE_EXECUTOR, issue, error));
263+
}
264+
}
265+
216266
@ObjectClassDefinition(name = "AEM Content Manager - Health Checker")
217267
public @interface Config {
218268

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,50 @@
11
package dev.vml.es.acm.core.instance;
22

33
import java.io.Serializable;
4+
import org.apache.commons.lang3.StringUtils;
45
import org.apache.commons.lang3.builder.ToStringBuilder;
56
import org.apache.commons.lang3.builder.ToStringStyle;
67

78
public class HealthIssue implements Serializable {
89

9-
private final String message;
10-
1110
private final HealthIssueSeverity severity;
1211

13-
public HealthIssue(HealthIssueSeverity severity, String message) {
12+
private final HealthIssueCategory category;
13+
14+
private final String issue;
15+
16+
private final String details;
17+
18+
public HealthIssue(HealthIssueSeverity severity, HealthIssueCategory category, String issue, String details) {
1419
this.severity = severity;
15-
this.message = message;
20+
this.issue = issue;
21+
this.category = category;
22+
this.details = details;
1623
}
1724

1825
public HealthIssueSeverity getSeverity() {
1926
return severity;
2027
}
2128

22-
public String getMessage() {
23-
return message;
29+
public HealthIssueCategory getCategory() {
30+
return category;
31+
}
32+
33+
public String getIssue() {
34+
return issue;
35+
}
36+
37+
public String getDetails() {
38+
return details;
2439
}
2540

2641
@Override
2742
public String toString() {
2843
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
2944
.append("severity", severity)
30-
.append("message", message)
45+
.append("issue", issue)
46+
.append("category", category)
47+
.append("details", StringUtils.abbreviate(details, 200))
3148
.toString();
3249
}
3350
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package dev.vml.es.acm.core.instance;
2+
3+
public enum HealthIssueCategory {
4+
REPOSITORY,
5+
OSGI,
6+
INSTALLER,
7+
INSTANCE,
8+
CODE_EXECUTOR,
9+
OTHER;
10+
}

core/src/main/java/dev/vml/es/acm/core/instance/HealthStatus.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.vml.es.acm.core.instance;
22

3+
import dev.vml.es.acm.core.util.ExceptionUtils;
34
import java.io.Serializable;
45
import java.util.LinkedList;
56
import java.util.List;
@@ -15,8 +16,8 @@ public class HealthStatus implements Serializable {
1516
public static HealthStatus exception(Exception e) {
1617
HealthStatus result = new HealthStatus();
1718
result.healthy = false;
18-
result.issues.add(
19-
new HealthIssue(HealthIssueSeverity.CRITICAL, String.format("Internal error : %s", e.getMessage())));
19+
result.issues.add(new HealthIssue(
20+
HealthIssueSeverity.CRITICAL, HealthIssueCategory.OTHER, "Internal error", ExceptionUtils.toString(e)));
2021
return result;
2122
}
2223

ui.frontend/src/components/HealthChecker.tsx

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,20 @@ import { Badge, Button, ButtonGroup, Cell, Column, Content, Dialog, DialogTrigge
22
import NoSearchResults from '@spectrum-icons/illustrations/NoSearchResults';
33
import Checkmark from '@spectrum-icons/workflow/Checkmark';
44
import Close from '@spectrum-icons/workflow/Close';
5+
import Code from '@spectrum-icons/workflow/Code';
6+
import Data from '@spectrum-icons/workflow/Data';
7+
import Gears from '@spectrum-icons/workflow/Gears';
58
import Help from '@spectrum-icons/workflow/Help';
9+
import Magnify from '@spectrum-icons/workflow/Magnify';
10+
import Question from '@spectrum-icons/workflow/Question';
611
import Replay from '@spectrum-icons/workflow/Replay';
712
import Settings from '@spectrum-icons/workflow/Settings';
13+
import UploadToCloud from '@spectrum-icons/workflow/UploadToCloud';
14+
import { ReactNode } from 'react';
815
import { useAppState } from '../hooks/app.ts';
916
import { HealthIssueSeverity, instancePrefix, InstanceType } from '../utils/api.types';
17+
import { Strings } from '../utils/strings.ts';
18+
import CodeEditor from './CodeEditor';
1019

1120
const HealthChecker = () => {
1221
const appState = useAppState();
@@ -23,6 +32,53 @@ const HealthChecker = () => {
2332
}
2433
};
2534

35+
const getCategoryFragment = (category: string): ReactNode => {
36+
switch (category) {
37+
case 'INSTANCE':
38+
return (
39+
<>
40+
<Settings size="S" />
41+
Instance
42+
</>
43+
);
44+
case 'REPOSITORY':
45+
return (
46+
<>
47+
<Data size="S" />
48+
Repository
49+
</>
50+
);
51+
case 'OSGI':
52+
return (
53+
<>
54+
<Gears size="S" />
55+
OSGi
56+
</>
57+
);
58+
case 'INSTALLER':
59+
return (
60+
<>
61+
<UploadToCloud size="S" />
62+
Installer
63+
</>
64+
);
65+
case 'CODE_EXECUTOR':
66+
return (
67+
<>
68+
<Code size="S" />
69+
Code executor
70+
</>
71+
);
72+
default:
73+
return (
74+
<>
75+
<Question size="S" />
76+
Other
77+
</>
78+
);
79+
}
80+
};
81+
2682
const renderEmptyState = () => (
2783
<IllustratedMessage>
2884
<NoSearchResults />
@@ -83,9 +139,11 @@ const HealthChecker = () => {
83139

84140
<TableView flex="1" aria-label="Health Issues" renderEmptyState={renderEmptyState} selectionMode="none" marginY="size-200" minHeight="size-3400">
85141
<TableHeader>
86-
<Column width="5%">#</Column>
87-
<Column>Severity</Column>
88-
<Column>Message</Column>
142+
<Column width="1fr">#</Column>
143+
<Column width="2fr">Severity</Column>
144+
<Column width="3fr">Category</Column>
145+
<Column width="8fr">Issue</Column>
146+
<Column width="2fr">Details</Column>
89147
</TableHeader>
90148
<TableBody>
91149
{(healthIssues || []).map((issue, index) => (
@@ -94,7 +152,43 @@ const HealthChecker = () => {
94152
<Cell>
95153
<Badge variant={getSeverityVariant(issue.severity)}>{issue.severity}</Badge>
96154
</Cell>
97-
<Cell>{issue.message}</Cell>
155+
<Cell>
156+
<Flex alignItems="center" gap="size-100">
157+
{getCategoryFragment(issue.category)}
158+
</Flex>
159+
</Cell>
160+
<Cell>
161+
<Text>{issue.issue}</Text>
162+
</Cell>
163+
<Cell>
164+
{Strings.notBlank(issue.details) ? (
165+
<DialogTrigger>
166+
<Button variant="secondary">
167+
<Magnify />
168+
<Text>View</Text>
169+
</Button>
170+
{(close) => (
171+
<Dialog width="75vw">
172+
<Heading>{issue.issue}</Heading>
173+
<Divider />
174+
<Content>
175+
<View height="size-4600">
176+
<CodeEditor id={`health-issue-${index}`} language="text" readOnly value={issue.details || 'No additional details available.'} />
177+
</View>
178+
</Content>
179+
<ButtonGroup>
180+
<Button variant="secondary" onPress={close}>
181+
<Close size="XS" />
182+
<Text>Close</Text>
183+
</Button>
184+
</ButtonGroup>
185+
</Dialog>
186+
)}
187+
</DialogTrigger>
188+
) : (
189+
<Text>&mdash;</Text>
190+
)}
191+
</Cell>
98192
</Row>
99193
))}
100194
</TableBody>

0 commit comments

Comments
 (0)