Skip to content

Commit 2d3ad44

Browse files
authored
Merge pull request #65 from exadel-inc/feature/ERM-64
[ERM-64] Add task history console
2 parents 46335ad + 8cb0b17 commit 2d3ad44

16 files changed

Lines changed: 609 additions & 25 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.exadel.etoolbox.rolloutmanager.core.models;
2+
3+
/**
4+
* Represents a logged event
5+
*/
6+
public class LogEvent {
7+
private final Type type;
8+
private final String path;
9+
private final Result result;
10+
11+
/**
12+
* Creates an instance of {@code LogEvent}
13+
* @param type Event type
14+
* @param path Target path
15+
* @param result Event result
16+
*/
17+
public LogEvent(Type type, String path, Result result) {
18+
this.type = type;
19+
this.path = path;
20+
this.result = result;
21+
}
22+
23+
/**
24+
* Retrieves the event type
25+
* @return {@link Type} value
26+
*/
27+
public Type getType() {
28+
return type;
29+
}
30+
31+
/**
32+
* Retrieves the target path
33+
* @return String value
34+
*/
35+
public String getPath() {
36+
return path;
37+
}
38+
39+
/**
40+
* Retrieves the event result
41+
* @return {@link Result} value
42+
*/
43+
public Result getResult() {
44+
return result;
45+
}
46+
47+
/**
48+
* Enumerates possible event results
49+
*/
50+
public enum Result {
51+
SUCCESS, ERROR
52+
}
53+
54+
/**
55+
* Enumerates possible event types
56+
*/
57+
public enum Type {
58+
ACTIVATION, ROLLOUT
59+
}
60+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package com.exadel.etoolbox.rolloutmanager.core.servlets;
2+
3+
import com.adobe.granite.ui.components.ds.DataSource;
4+
import com.adobe.granite.ui.components.ds.SimpleDataSource;
5+
import com.adobe.granite.ui.components.ds.ValueMapResource;
6+
import com.exadel.etoolbox.rolloutmanager.core.models.LogEvent;
7+
import com.exadel.etoolbox.rolloutmanager.core.models.RolloutItem;
8+
import com.exadel.etoolbox.rolloutmanager.core.services.impl.RolloutExecutor;
9+
import com.exadel.etoolbox.rolloutmanager.core.utils.RolloutLogUtil;
10+
import com.exadel.etoolbox.rolloutmanager.core.utils.RolloutPlanUtil;
11+
import com.exadel.etoolbox.rolloutmanager.core.utils.ThrottledLogger;
12+
import org.apache.commons.lang3.ArrayUtils;
13+
import org.apache.commons.lang3.StringUtils;
14+
import org.apache.jackrabbit.JcrConstants;
15+
import org.apache.sling.api.SlingHttpServletRequest;
16+
import org.apache.sling.api.SlingHttpServletResponse;
17+
import org.apache.sling.api.resource.Resource;
18+
import org.apache.sling.api.resource.ValueMap;
19+
import org.apache.sling.api.servlets.ServletResolverConstants;
20+
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
21+
import org.apache.sling.api.wrappers.ValueMapDecorator;
22+
import org.apache.sling.event.jobs.Job;
23+
import org.apache.sling.event.jobs.JobManager;
24+
import org.osgi.service.component.annotations.Component;
25+
import org.osgi.service.component.annotations.Reference;
26+
27+
import javax.servlet.Servlet;
28+
import java.text.SimpleDateFormat;
29+
import java.util.ArrayList;
30+
import java.util.Arrays;
31+
import java.util.HashMap;
32+
import java.util.List;
33+
import java.util.Map;
34+
import java.util.Objects;
35+
import java.util.stream.Collectors;
36+
import java.util.stream.Stream;
37+
38+
@Component(
39+
service = Servlet.class,
40+
property = ServletResolverConstants.SLING_SERVLET_RESOURCE_TYPES + "=etoolbox-rollout-manager/components/historyDatasource"
41+
)
42+
public class HistoryDatasource extends SlingSafeMethodsServlet {
43+
44+
private static final String PARAM_LIMIT = "limit";
45+
private static final String PARAM_OFFSET = "offset";
46+
47+
private static final int DEFAULT_LIMIT = 40;
48+
private static final int NO_LIMIT = 0;
49+
private static final Map<String,Object>[] NO_FILTER = null;
50+
51+
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
52+
53+
@Reference
54+
private transient JobManager jobManager;
55+
56+
@Override
57+
protected void doGet(
58+
SlingHttpServletRequest request,
59+
SlingHttpServletResponse response) {
60+
61+
String offsetParam = request.getParameter(PARAM_OFFSET);
62+
int offset = StringUtils.isNumeric(offsetParam) ? Integer.parseInt(offsetParam) : 0;
63+
String limitParam = request.getParameter(PARAM_LIMIT);
64+
if (StringUtils.isEmpty(limitParam)) {
65+
limitParam = request.getResource().getValueMap().get(PARAM_LIMIT, String.class);
66+
}
67+
int limit = (StringUtils.isNumeric(limitParam) ? Integer.parseInt(limitParam) : DEFAULT_LIMIT) + 1;
68+
69+
List<Job> jobs = new ArrayList<>(jobManager.findJobs(JobManager.QueryType.ALL, RolloutExecutor.TOPIC, NO_LIMIT, NO_FILTER));
70+
jobs.addAll(jobManager.findJobs(JobManager.QueryType.HISTORY, RolloutExecutor.TOPIC, NO_LIMIT, NO_FILTER));
71+
jobs.sort((j1, j2) -> Long.compare(j2.getCreated().getTimeInMillis(), j1.getCreated().getTimeInMillis()));
72+
73+
jobs = jobs.subList(offset, Math.min(jobs.size(), offset + limit));
74+
75+
List<Resource> resources = new ArrayList<>();
76+
SimpleDateFormat dateFormat = null;
77+
78+
for (Job job : jobs) {
79+
if (dateFormat == null) {
80+
dateFormat = new SimpleDateFormat(DATE_FORMAT);
81+
}
82+
83+
Map<String, Object> props = new HashMap<>();
84+
props.put(
85+
"status",
86+
getStatusString(job));
87+
props.put(
88+
"dateCreated",
89+
dateFormat.format(job.getCreated().getTime()));
90+
props.put(
91+
"dateStarted",
92+
job.getProcessingStarted() != null ? dateFormat.format(job.getProcessingStarted().getTime()) : null);
93+
props.put(
94+
"dateFinished",
95+
job.getFinishedDate() != null ? dateFormat.format(job.getFinishedDate().getTime()) : null);
96+
props.put(
97+
"initiator",
98+
job.getProperty(RolloutExecutor.PROPERTY_USER, String.class));
99+
JobDetails jobDetails = new JobDetails(job);
100+
props.put(
101+
"source",
102+
jobDetails.getSource());
103+
props.put(
104+
"target",
105+
jobDetails.getTargets().stream().map(t -> getResultHtml(t, jobDetails)).collect(Collectors.toList()));
106+
107+
ValueMap valueMap = new ValueMapDecorator(props);
108+
Resource resource = new ValueMapResource(
109+
request.getResourceResolver(),
110+
request.getRequestPathInfo().getResourcePath() + "/item_" + job.getId(),
111+
JcrConstants.NT_UNSTRUCTURED,
112+
valueMap);
113+
resources.add(resource);
114+
}
115+
116+
if (resources.isEmpty()) {
117+
return;
118+
}
119+
request.setAttribute(DataSource.class.getName(), new SimpleDataSource(resources.iterator()));
120+
}
121+
122+
private static String getStatusString(Job job) {
123+
Job.JobState state = job.getJobState();
124+
if (state == Job.JobState.SUCCEEDED) {
125+
return "success";
126+
}
127+
if (state == Job.JobState.ERROR
128+
|| state == Job.JobState.GIVEN_UP
129+
|| state == Job.JobState.DROPPED
130+
|| state == Job.JobState.STOPPED) {
131+
return "failure";
132+
}
133+
return state.name().toLowerCase();
134+
}
135+
136+
private static String getResultHtml(String target, JobDetails details) {
137+
LogEvent.Result activationResult = details.getActivationResult(target);
138+
LogEvent.Result rolloutResult = details.getRolloutResult(target);
139+
StringBuilder sb = new StringBuilder();
140+
if (rolloutResult != null) {
141+
sb.append("<coral-icon icon=\"")
142+
.append(rolloutResult == LogEvent.Result.SUCCESS ? "check" : "close")
143+
.append("\"></coral-icon>");
144+
}
145+
sb.append(target);
146+
if (activationResult != null) {
147+
sb.append(" <span class=\"activation")
148+
.append(activationResult == LogEvent.Result.SUCCESS ? " success" : " error")
149+
.append("\">Activate</span>");
150+
}
151+
return sb.toString();
152+
}
153+
154+
private static class JobDetails {
155+
private Map<String, LogEvent.Result> activationReports;
156+
private Map<String, LogEvent.Result> rolloutReports;
157+
private String source;
158+
private List<String> targets;
159+
160+
JobDetails(Job job) {
161+
String plan = job.getProperty(RolloutExecutor.PROPERTY_PLAN, String.class);
162+
RolloutItem[] items = RolloutPlanUtil.getItems(plan);
163+
if (ArrayUtils.isEmpty(items)) {
164+
return;
165+
}
166+
assert items != null;
167+
Arrays.sort(items);
168+
source = items[0].getMaster();
169+
targets = Stream.of(items)
170+
.map(RolloutItem::getTarget)
171+
.distinct()
172+
.collect(Collectors.toList());
173+
174+
String[] log = job.getProgressLog();
175+
if (ArrayUtils.isEmpty(log)) {
176+
return;
177+
}
178+
Stream.of(log)
179+
.flatMap(entry -> StringUtils.contains(entry, ThrottledLogger.ENTRY_SEPARATOR)
180+
? Arrays.stream(StringUtils.split(entry, ThrottledLogger.ENTRY_SEPARATOR))
181+
: Stream.of(entry))
182+
.filter(StringUtils::isNotBlank)
183+
.map(RolloutLogUtil::getEvent)
184+
.filter(Objects::nonNull)
185+
.forEach(e -> {
186+
if (e.getType() == LogEvent.Type.ACTIVATION) {
187+
if (activationReports == null) {
188+
activationReports = new HashMap<>();
189+
}
190+
activationReports.put(e.getPath(), e.getResult());
191+
} else if (e.getType() == LogEvent.Type.ROLLOUT) {
192+
if (rolloutReports == null) {
193+
rolloutReports = new HashMap<>();
194+
}
195+
rolloutReports.put(e.getPath(), e.getResult());
196+
}
197+
});
198+
}
199+
200+
public LogEvent.Result getActivationResult(String path) {
201+
return activationReports != null ? activationReports.get(path) : null;
202+
}
203+
204+
public LogEvent.Result getRolloutResult(String path) {
205+
return rolloutReports != null ? rolloutReports.get(path) : null;
206+
}
207+
208+
public String getSource() {
209+
return source;
210+
}
211+
212+
public List<String> getTargets() {
213+
return targets;
214+
}
215+
}
216+
}

core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/utils/RolloutLogUtil.java

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
package com.exadel.etoolbox.rolloutmanager.core.utils;
22

3+
import com.exadel.etoolbox.rolloutmanager.core.models.LogEvent;
4+
import com.fasterxml.jackson.core.JsonProcessingException;
5+
import com.fasterxml.jackson.databind.JsonNode;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import com.fasterxml.jackson.databind.node.ObjectNode;
8+
import org.apache.commons.lang3.EnumUtils;
39
import org.apache.commons.lang3.StringUtils;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
412

513
import javax.json.Json;
614
import javax.json.JsonArrayBuilder;
@@ -13,9 +21,16 @@
1321
*/
1422
public class RolloutLogUtil {
1523

24+
private static final Logger LOG = LoggerFactory.getLogger(RolloutLogUtil.class);
25+
26+
private static final String PROPERTY_PATH = "path";
1627
private static final String PROPERTY_RESULT = "result";
1728
private static final String PROPERTY_TYPE = "type";
1829

30+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
31+
private static final String RESULT_ERROR = "error";
32+
private static final String RESULT_SUCCESS = "success";
33+
1934
private RolloutLogUtil() {
2035
}
2136

@@ -55,14 +70,48 @@ public static void logEvent(Consumer<String> logger, String type, String target)
5570
public static void logEvent(Consumer<String> logger, String type, String target, String errorMessage) {
5671
JsonObjectBuilder builder = Json.createObjectBuilder()
5772
.add(PROPERTY_TYPE, type)
58-
.add("path", target);
73+
.add(PROPERTY_PATH, target);
5974
if (StringUtils.isNotEmpty(errorMessage)) {
60-
builder.add(PROPERTY_RESULT, "error");
61-
builder.add("error", errorMessage);
75+
builder.add(PROPERTY_RESULT, RESULT_ERROR);
76+
builder.add(RESULT_ERROR, errorMessage);
6277
} else {
63-
builder.add(PROPERTY_RESULT, "success");
78+
builder.add(PROPERTY_RESULT, RESULT_SUCCESS);
6479
}
6580
String message = builder.build().toString();
6681
logger.accept(message);
6782
}
83+
84+
/**
85+
* Parses a JSON string representation of a log event into a {@link LogEvent} object
86+
* @param json JSON string representation of a log event
87+
* @return A {@link LogEvent} object; null if parsing fails
88+
*/
89+
public static LogEvent getEvent(String json) {
90+
try {
91+
JsonNode jsonNode = OBJECT_MAPPER.readTree(json);
92+
if (!(jsonNode instanceof ObjectNode)) {
93+
throw new IllegalArgumentException("Invalid JSON");
94+
}
95+
96+
ObjectNode objectNode = (ObjectNode) jsonNode;
97+
String typeStr = objectNode.has(PROPERTY_TYPE) ? objectNode.get(PROPERTY_TYPE).asText() : null;
98+
LogEvent.Type type = EnumUtils.getEnumIgnoreCase(LogEvent.Type.class, typeStr);
99+
String path = objectNode.has(PROPERTY_PATH) ? objectNode.get(PROPERTY_PATH).asText() : null;
100+
101+
if (type == null || StringUtils.isEmpty(path)) {
102+
throw new IllegalArgumentException("Missing required properties");
103+
}
104+
105+
String resultStr = objectNode.has(PROPERTY_RESULT) ? objectNode.get(PROPERTY_RESULT).asText() : null;
106+
LogEvent.Result result = RESULT_SUCCESS.equals(resultStr)
107+
? LogEvent.Result.SUCCESS
108+
: LogEvent.Result.ERROR;
109+
110+
return new LogEvent(type, path, result);
111+
112+
} catch (JsonProcessingException | IllegalArgumentException e) {
113+
LOG.error("Failed to parse log event: {}", json, e);
114+
}
115+
return null;
116+
}
68117
}

core/src/main/java/com/exadel/etoolbox/rolloutmanager/core/utils/RolloutPlanUtil.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.exadel.etoolbox.rolloutmanager.core.models.RolloutItem;
44
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import org.apache.commons.lang3.StringUtils;
56
import org.slf4j.Logger;
67
import org.slf4j.LoggerFactory;
78

@@ -25,6 +26,10 @@ private RolloutPlanUtil() {
2526
* @return array of {@link RolloutItem} objects; null if mapping fails
2627
*/
2728
public static RolloutItem[] getItems(String source) {
29+
if (StringUtils.isEmpty(source)) {
30+
LOG.warn("Source is empty");
31+
return null;
32+
}
2833
try {
2934
return OBJECT_MAPPER.readValue(source, RolloutItem[].class);
3035
} catch (IOException | IllegalArgumentException e) {

ui.apps.structure/pom.xml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,12 @@
4848
<filters>
4949
<!-- /apps root -->
5050
<filter><root>/apps</root></filter>
51+
<filter><root>/apps/cq/core/content/nav/tools/etoolbox</root></filter>
5152
<filter><root>/apps/etoolbox-rollout-manager</root></filter>
5253
<filter><root>/apps/wcm/core/content/sites/jcr:content/actions/selection</root></filter>
53-
54-
5554
</filters>
5655
</configuration>
5756
</plugin>
5857
</plugins>
5958
</build>
60-
</project>
59+
</project>

0 commit comments

Comments
 (0)