1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.codelibs.fess.job;
17
18 import static org.codelibs.core.stream.StreamUtil.stream;
19
20 import java.time.Instant;
21 import java.time.ZoneId;
22 import java.time.format.DateTimeFormatter;
23 import java.util.ArrayList;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.stream.Collectors;
27
28 import org.apache.logging.log4j.LogManager;
29 import org.apache.logging.log4j.Logger;
30 import org.codelibs.core.lang.StringUtil;
31 import org.codelibs.fess.helper.LogNotificationHelper.LogNotificationEvent;
32 import org.codelibs.fess.helper.NotificationHelper;
33 import org.codelibs.fess.helper.SystemHelper;
34 import org.codelibs.fess.mylasta.direction.FessConfig;
35 import org.codelibs.fess.mylasta.mail.LogNotificationPostcard;
36 import org.codelibs.fess.opensearch.client.SearchEngineClient;
37 import org.codelibs.fess.util.ComponentUtil;
38 import org.dbflute.mail.send.hook.SMailCallbackContext;
39 import org.lastaflute.core.mail.Postbox;
40 import org.opensearch.action.bulk.BulkRequestBuilder;
41 import org.opensearch.action.search.SearchResponse;
42 import org.opensearch.index.query.QueryBuilders;
43 import org.opensearch.search.SearchHit;
44 import org.opensearch.search.sort.SortOrder;
45
46
47
48
49 public class LogNotificationJob {
50
51
52
53
54 public LogNotificationJob() {
55
56 }
57
58 private static final Logger logger = LogManager.getLogger(LogNotificationJob.class);
59
60 private static final DateTimeFormatter TIMESTAMP_FORMATTER =
61 DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS").withZone(ZoneId.systemDefault());
62
63
64
65
66
67
68 public String execute() {
69 final FessConfig fessConfig = ComponentUtil.getFessConfig();
70
71 if (!fessConfig.isLogNotificationEnabled()) {
72 return "Log notification disabled.";
73 }
74
75 if (!fessConfig.hasNotification()) {
76 return "No notification targets configured.";
77 }
78
79 final SearchEngineClient client = ComponentUtil.getSearchEngineClient();
80 final String indexName = fessConfig.getIndexLogIndex() + ".notification_queue";
81
82 if (!client.existsIndex(indexName)) {
83 return "No log notifications.";
84 }
85
86 final int searchSize = fessConfig.getLogNotificationSearchSizeAsInteger();
87 final String hostname = ComponentUtil.getSystemHelper().getHostname();
88 final SearchResponse searchResponse = client.prepareSearch(indexName)
89 .setQuery(QueryBuilders.termQuery("hostname", hostname))
90 .setSize(searchSize)
91 .addSort("timestamp", SortOrder.ASC)
92 .execute()
93 .actionGet(fessConfig.getIndexSearchTimeout());
94
95 final SearchHit[] hits = searchResponse.getHits().getHits();
96 if (hits.length == 0) {
97 return "No log notifications.";
98 }
99
100 final List<LogNotificationEvent> events = new ArrayList<>();
101 final List<String> docIds = new ArrayList<>();
102 for (final SearchHit hit : hits) {
103 final Map<String, Object> source = hit.getSourceAsMap();
104 events.add(new LogNotificationEvent(((Number) source.get("timestamp")).longValue(), (String) source.get("level"),
105 (String) source.get("loggerName"), (String) source.get("message"), (String) source.get("throwable")));
106 docIds.add(hit.getId());
107 }
108
109 final int maxDetailsLength = fessConfig.getLogNotificationMaxDetailsLengthAsInteger();
110 final int maxDisplayEvents = fessConfig.getLogNotificationMaxDisplayEventsAsInteger();
111 final int maxMessageLength = fessConfig.getLogNotificationMaxMessageLengthAsInteger();
112
113 final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
114 final Map<String, List<LogNotificationEvent>> eventsByLevel =
115 events.stream().collect(Collectors.groupingBy(LogNotificationEvent::getLevel));
116
117 for (final Map.Entry<String, List<LogNotificationEvent>> entry : eventsByLevel.entrySet()) {
118 final String level = entry.getKey();
119 final List<LogNotificationEvent> levelEvents = entry.getValue();
120 final String details = formatDetails(levelEvents, maxDetailsLength, maxDisplayEvents, maxMessageLength);
121
122 final String toStrs = fessConfig.getNotificationTo();
123 final String[] toAddresses;
124 if (StringUtil.isNotBlank(toStrs)) {
125 toAddresses = toStrs.split(",");
126 } else {
127 toAddresses = StringUtil.EMPTY_STRINGS;
128 }
129
130 final Postbox postbox = ComponentUtil.getComponent(Postbox.class);
131 final NotificationHelper notificationHelper = ComponentUtil.getNotificationHelper();
132 try {
133 SMailCallbackContext.setPreparedMessageHookOnThread(notificationHelper::send);
134 LogNotificationPostcard.droppedInto(postbox, postcard -> {
135 postcard.setFrom(fessConfig.getMailFromAddress(), fessConfig.getMailFromName());
136 postcard.addReplyTo(fessConfig.getMailReturnPath());
137 if (toAddresses.length > 0) {
138 stream(toAddresses).of(stream -> stream.map(String::trim).forEach(address -> {
139 postcard.addTo(address);
140 }));
141 } else {
142 postcard.addTo(fessConfig.getMailFromAddress());
143 postcard.dryrun();
144 }
145 postcard.setHostname(systemHelper.getHostname());
146 postcard.setLevel(level);
147 postcard.setCount(String.valueOf(levelEvents.size()));
148 postcard.setInterval(String.valueOf(fessConfig.getLogNotificationIntervalAsInteger()));
149 postcard.setDetails(details);
150 });
151 } catch (final Exception e) {
152 logger.warn("Failed to send log notification.", e);
153 } finally {
154 SMailCallbackContext.clearPreparedMessageHookOnThread();
155 }
156 }
157
158 final BulkRequestBuilder bulkDelete = client.prepareBulk();
159 for (final String docId : docIds) {
160 bulkDelete.add(client.prepareDelete().setIndex(indexName).setId(docId));
161 }
162 bulkDelete.execute().actionGet(fessConfig.getIndexSearchTimeout());
163
164
165 try {
166 client.deleteByQuery(indexName, QueryBuilders.termQuery("hostname", hostname));
167 } catch (final Exception e) {
168 logger.debug("Failed to delete remaining log notifications.", e);
169 }
170
171 return "Sent log notifications: " + events.size() + " events.";
172 }
173
174
175
176
177
178
179
180
181
182
183 protected String formatDetails(final List<LogNotificationEvent> events, final int maxDetailsLength, final int maxDisplayEvents,
184 final int maxMessageLength) {
185 final int totalCount = events.size();
186 final int displayCount = Math.min(totalCount, maxDisplayEvents);
187 final StringBuilder sb = new StringBuilder();
188 sb.append("Total: ").append(totalCount).append(" event(s)");
189 if (totalCount > displayCount) {
190 sb.append(" (showing ").append(displayCount).append(')');
191 }
192 sb.append("\n\n");
193
194 for (int i = 0; i < displayCount; i++) {
195 final LogNotificationEvent event = events.get(i);
196 final String timestamp = TIMESTAMP_FORMATTER.format(Instant.ofEpochMilli(event.getTimestamp()));
197 String message = event.getMessage();
198 if (message != null && message.length() > maxMessageLength) {
199 message = message.substring(0, maxMessageLength) + "...";
200 }
201 sb.append('[')
202 .append(timestamp)
203 .append("] ")
204 .append(event.getLevel())
205 .append(' ')
206 .append(event.getLoggerName())
207 .append(" - ")
208 .append(message)
209 .append('\n');
210 if (sb.length() > maxDetailsLength) {
211 sb.setLength(maxDetailsLength);
212 break;
213 }
214 }
215
216 if (totalCount > displayCount) {
217 sb.append("... and ").append(totalCount - displayCount).append(" more\n");
218 }
219
220 return sb.toString();
221 }
222 }