001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 2016, 2020 Michael N. Lipp 004 * 005 * This program is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU Affero General Public License as published by 007 * the Free Software Foundation; either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, but 011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 013 * for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License along 016 * with this program; if not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jgrapes.webconlet.logviewer; 020 021import freemarker.core.ParseException; 022import freemarker.template.MalformedTemplateNameException; 023import freemarker.template.Template; 024import freemarker.template.TemplateNotFoundException; 025import java.io.ByteArrayOutputStream; 026import java.io.IOException; 027import java.io.PrintWriter; 028import java.io.Serializable; 029import java.util.HashMap; 030import java.util.HashSet; 031import java.util.Map; 032import java.util.Optional; 033import java.util.Set; 034import java.util.logging.LogRecord; 035import org.jgrapes.core.Channel; 036import org.jgrapes.core.Event; 037import org.jgrapes.core.Manager; 038import org.jgrapes.core.annotation.Handler; 039import org.jgrapes.webconsole.base.Conlet.RenderMode; 040import org.jgrapes.webconsole.base.ConsoleConnection; 041import org.jgrapes.webconsole.base.WebConsoleUtils; 042import org.jgrapes.webconsole.base.events.AddConletType; 043import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource; 044import org.jgrapes.webconsole.base.events.ConsoleReady; 045import org.jgrapes.webconsole.base.events.NotifyConletModel; 046import org.jgrapes.webconsole.base.events.NotifyConletView; 047import org.jgrapes.webconsole.base.events.RenderConlet; 048import org.jgrapes.webconsole.base.events.RenderConletRequestBase; 049import org.jgrapes.webconsole.base.events.SetLocale; 050import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; 051 052/** 053 * A conlet for displaying records from java.util.logging. The class 054 * {@link LogViewerHandler} has to be registered as a handler in the 055 * logging configuration (e.g. with 056 * `-Djava.util.logging.config.file=logging.properties` and 057 * `logging.properties`: 058 * ``` 059 * org.jgrapes.webconlet.logviewer.LogViewerHandler.level=CONFIG 060 * ``` 061 * 062 * The handler implements a ring buffer for the last 100 063 * {@link LogRecord}s. When the conlet is displayed, it obtains 064 * the initially shown messages from the ring buffer. All subsequently 065 * published {@link LogRecord}s are forwarded to the conlet. 066 * In order to limit the memory required in the browser, the conlet 067 * also retains only the 100 most recent messages. 068 */ 069public class LogViewerConlet extends FreeMarkerConlet<Serializable> { 070 071 private static final Set<RenderMode> MODES 072 = RenderMode.asSet(RenderMode.View); 073 074 /** 075 * Creates a new component with its channel set to the given channel. 076 * 077 * @param componentChannel the channel that the component's handlers listen 078 * on by default and that {@link Manager#fire(Event, Channel...)} 079 * sends the event to 080 */ 081 public LogViewerConlet(Channel componentChannel) { 082 super(componentChannel); 083 } 084 085 /** 086 * On {@link ConsoleReady}, fire the {@link AddConletType}. 087 * 088 * @param event the event 089 * @param channel the channel 090 * @throws TemplateNotFoundException the template not found exception 091 * @throws MalformedTemplateNameException the malformed template name 092 * exception 093 * @throws ParseException the parse exception 094 * @throws IOException Signals that an I/O exception has occurred. 095 */ 096 @Handler 097 public void onConsoleReady(ConsoleReady event, ConsoleConnection channel) 098 throws TemplateNotFoundException, MalformedTemplateNameException, 099 ParseException, IOException { 100 // Add conlet resources to page 101 channel.respond(new AddConletType(type()) 102 .setDisplayNames( 103 localizations(channel.supportedLocales(), "conletName")) 104 .addRenderMode(RenderMode.View) 105 .addScript(new ScriptResource().setScriptType("module") 106 .setScriptUri(event.renderSupport().conletResource( 107 type(), "LogViewer-functions.ftl.js"))) 108 .addCss(event.renderSupport(), 109 WebConsoleUtils.uriFromPath("LogViewer-style.css"))); 110 } 111 112 @Override 113 protected Set<RenderMode> doRenderConlet(RenderConletRequestBase<?> event, 114 ConsoleConnection channel, String conletId, 115 Serializable conletState) 116 throws Exception { 117 Set<RenderMode> renderedAs = new HashSet<>(); 118 if (event.renderAs().contains(RenderMode.View)) { 119 Template tpl 120 = freemarkerConfig().getTemplate("LogViewer-view.ftl.html"); 121 channel.respond(new RenderConlet(type(), conletId, 122 processTemplate(event, tpl, 123 fmModel(event, channel, conletId, conletState))) 124 .setRenderAs( 125 RenderMode.View.addModifiers(event.renderAs())) 126 .setSupportedModes(MODES)); 127 sendAllEntries(channel, conletId); 128 renderedAs.add(RenderMode.View); 129 } 130 return renderedAs; 131 } 132 133 private void sendAllEntries(ConsoleConnection channel, String conletId) { 134 channel.respond(new NotifyConletView(type(), 135 conletId, "entries", 136 (Object) LogViewerHandler.setConlet(this).stream() 137 .map(this::logEntryAsMap).toArray())); 138 } 139 140 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 141 /* default */ void addEntry(LogRecord entry) { 142 for (ConsoleConnection connection : trackedConnections()) { 143 for (String conletId : conletIds(connection)) { 144 connection.respond(new NotifyConletView(type(), 145 conletId, "addEntry", logEntryAsMap(entry)) 146 .disableTracking()); 147 } 148 } 149 } 150 151 private Map<String, Object> logEntryAsMap(LogRecord record) { 152 @SuppressWarnings("PMD.UseConcurrentHashMap") 153 Map<String, Object> result = new HashMap<>(); 154 result.put("exception", Optional.ofNullable(record.getThrown()) 155 .map(Throwable::getMessage).orElse("")); 156 result.put("stacktrace", Optional.ofNullable(record.getThrown()) 157 .map(exc -> { 158 ByteArrayOutputStream out = new ByteArrayOutputStream(); 159 PrintWriter printWriter = new PrintWriter(out); 160 exc.printStackTrace(printWriter); 161 printWriter.close(); 162 return out.toString(); 163 }).orElse("")); 164 result.put("loggerName", record.getLoggerName()); 165 result.put("source", 166 record.getSourceClassName() + "::" + record.getSourceMethodName()); 167 result.put("logLevel", record.getLevel().toString()); 168 result.put("message", record.getMessage()); 169 result.put("time", record.getInstant().toEpochMilli()); 170 result.put("sequence", record.getSequenceNumber()); 171 return result; 172 } 173 174 /* 175 * (non-Javadoc) 176 * 177 * @see org.jgrapes.console.AbstractConlet#doNotifyConletModel 178 */ 179 @Override 180 @SuppressWarnings({ "PMD.SwitchStmtsShouldHaveDefault", 181 "PMD.TooFewBranchesForASwitchStatement" }) 182 protected void doUpdateConletState(NotifyConletModel event, 183 ConsoleConnection channel, Serializable conletState) 184 throws Exception { 185 event.stop(); 186 switch (event.method()) { 187 case "resync": 188 sendAllEntries(channel, event.conletId()); 189 break; 190 } 191 } 192 193 @Override 194 protected boolean doSetLocale(SetLocale event, ConsoleConnection channel, 195 String conletId) throws Exception { 196 return true; 197 } 198}