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.osgi.webconlet.logviewer; 020 021import de.mnl.osgi.coreutils.ServiceCollector; 022import freemarker.core.ParseException; 023import freemarker.template.MalformedTemplateNameException; 024import freemarker.template.Template; 025import freemarker.template.TemplateNotFoundException; 026import java.io.ByteArrayOutputStream; 027import java.io.IOException; 028import java.io.PrintWriter; 029import java.util.Collections; 030import java.util.HashMap; 031import java.util.HashSet; 032import java.util.Map; 033import java.util.Optional; 034import java.util.Set; 035import org.jgrapes.core.Channel; 036import org.jgrapes.core.Event; 037import org.jgrapes.core.Manager; 038import org.jgrapes.core.annotation.Handler; 039import org.jgrapes.core.events.Stop; 040import org.jgrapes.http.Session; 041import org.jgrapes.webconsole.base.AbstractConlet; 042import org.jgrapes.webconsole.base.Conlet.RenderMode; 043import org.jgrapes.webconsole.base.ConsoleSession; 044import org.jgrapes.webconsole.base.WebConsoleUtils; 045import org.jgrapes.webconsole.base.events.AddConletRequest; 046import org.jgrapes.webconsole.base.events.AddConletType; 047import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource; 048import org.jgrapes.webconsole.base.events.ConsoleReady; 049import org.jgrapes.webconsole.base.events.NotifyConletModel; 050import org.jgrapes.webconsole.base.events.NotifyConletView; 051import org.jgrapes.webconsole.base.events.RenderConletRequest; 052import org.jgrapes.webconsole.base.events.RenderConletRequestBase; 053import org.jgrapes.webconsole.base.events.SetLocale; 054import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet; 055import org.osgi.framework.BundleContext; 056import org.osgi.framework.InvalidSyntaxException; 057import org.osgi.service.log.LogEntry; 058import org.osgi.service.log.LogListener; 059import org.osgi.service.log.LogReaderService; 060 061/** 062 * A conlet for displaying the OSGi log. 063 */ 064public class LogViewerConlet 065 extends FreeMarkerConlet<AbstractConlet.ConletBaseModel> { 066 067 private static final Set<RenderMode> MODES 068 = RenderMode.asSet(RenderMode.View); 069 private ServiceCollector<LogReaderService, 070 LogReaderService> logReaderCollector; 071 private LogReaderService logReaderResolved; 072 private LogListener logReaderListener; 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 @SuppressWarnings("PMD.UnusedFormalParameter") 082 public LogViewerConlet(Channel componentChannel, BundleContext context) { 083 super(componentChannel); 084 logReaderListener = new LogListener() { 085 @Override 086 public void logged(LogEntry entry) { 087 addEntry(entry); 088 } 089 }; 090 logReaderCollector 091 = new ServiceCollector<>(context, LogReaderService.class); 092 logReaderCollector.setOnBound((ref, svc) -> subscribeTo(svc)) 093 .setOnModfied((ref, svc) -> subscribeTo(svc)) 094 .setOnUnbinding((ref, svc) -> subscribeTo(svc)); 095 logReaderCollector.open(); 096 } 097 098 private void subscribeTo(LogReaderService logReaderService) { 099 if (logReaderResolved != null 100 && logReaderResolved.equals(logReaderService)) { 101 return; 102 } 103 // Got a new log reader service. 104 if (logReaderResolved != null) { 105 logReaderResolved.removeLogListener(logReaderListener); 106 } 107 logReaderResolved = logReaderService; 108 if (logReaderResolved != null) { 109 logReaderResolved.addLogListener(logReaderListener); 110 } 111 112 } 113 114 @Handler(channels = Channel.class) 115 public void onStop(Stop event) { 116 logReaderCollector.close(); 117 } 118 119 /** 120 * On {@link ConsoleReady}, fire the {@link AddConletType}. 121 * 122 * @param event the event 123 * @param channel the channel 124 * @throws TemplateNotFoundException the template not found exception 125 * @throws MalformedTemplateNameException the malformed template name 126 * exception 127 * @throws ParseException the parse exception 128 * @throws IOException Signals that an I/O exception has occurred. 129 */ 130 @Handler 131 public void onConsoleReady(ConsoleReady event, ConsoleSession channel) 132 throws TemplateNotFoundException, MalformedTemplateNameException, 133 ParseException, IOException { 134 // Add conlet resources to page 135 channel.respond(new AddConletType(type()) 136 .setDisplayNames( 137 localizations(channel.supportedLocales(), "conletName")) 138 .addRenderMode(RenderMode.View) 139 .addScript(new ScriptResource() 140 .setScriptUri(event.renderSupport().conletResource( 141 type(), "LogViewer-functions.ftl.js")) 142 .setScriptType("module")) 143 .addCss(event.renderSupport(), 144 WebConsoleUtils.uriFromPath("LogViewer-style.css"))); 145 } 146 147 /* 148 * (non-Javadoc) 149 * 150 * @see org.jgrapes.console.AbstractConlet#generateConletId() 151 */ 152 @Override 153 protected String generateConletId() { 154 return type() + "-" + super.generateConletId(); 155 } 156 157 /* 158 * (non-Javadoc) 159 * 160 * @see org.jgrapes.console.AbstractConlet#modelFromSession 161 */ 162 @SuppressWarnings("PMD.AvoidDuplicateLiterals") 163 @Override 164 protected Optional<ConletBaseModel> stateFromSession( 165 Session session, String conletId) { 166 if (conletId.startsWith(type() + "-")) { 167 return Optional.of(new ConletBaseModel(conletId)); 168 } 169 return Optional.empty(); 170 } 171 172 /* 173 * (non-Javadoc) 174 * 175 * @see org.jgrapes.console.AbstractConlet#doAddConlet 176 */ 177 @Override 178 protected ConletTrackingInfo doAddConlet(AddConletRequest event, 179 ConsoleSession channel) throws Exception { 180 ConletBaseModel conletModel 181 = new ConletBaseModel(generateConletId()); 182 return new ConletTrackingInfo(conletModel.getConletId()) 183 .addModes(renderConlet(event, channel, conletModel)); 184 } 185 186 /* 187 * (non-Javadoc) 188 * 189 * @see org.jgrapes.console.AbstractConlet#doRenderConlet 190 */ 191 @Override 192 protected Set<RenderMode> doRenderConlet(RenderConletRequest event, 193 ConsoleSession channel, String conletId, 194 ConletBaseModel conletModel) throws Exception { 195 return renderConlet(event, channel, conletModel); 196 } 197 198 private Set<RenderMode> renderConlet(RenderConletRequestBase<?> event, 199 ConsoleSession channel, ConletBaseModel conletModel) 200 throws TemplateNotFoundException, 201 MalformedTemplateNameException, ParseException, IOException, 202 InvalidSyntaxException { 203 Set<RenderMode> renderedAs = new HashSet<>(); 204 if (event.renderAs().contains(RenderMode.View)) { 205 Template tpl 206 = freemarkerConfig().getTemplate("LogViewer-view.ftl.html"); 207 channel.respond(new RenderConletFromTemplate(event, 208 type(), conletModel.getConletId(), tpl, 209 fmModel(event, channel, conletModel)) 210 .setRenderAs(RenderMode.View.addModifiers(event.renderAs())) 211 .setSupportedModes(MODES)); 212 sendAllEntries(channel, conletModel.getConletId()); 213 renderedAs.add(RenderMode.View); 214 } 215 return renderedAs; 216 } 217 218 private void sendAllEntries(ConsoleSession channel, String conletId) { 219 final LogReaderService logReader = logReaderResolved; 220 if (logReader == null) { 221 return; 222 } 223 channel.respond(new NotifyConletView(type(), 224 conletId, "entries", 225 (Object) Collections.list(logReader.getLog()).stream() 226 .map(entry -> logEntryAsMap(entry)).toArray())); 227 } 228 229 private void addEntry(LogEntry entry) { 230 for (ConsoleSession consoleSession : trackedSessions()) { 231 for (String conletId : conletIds(consoleSession)) { 232 consoleSession.respond(new NotifyConletView(type(), 233 conletId, "addEntry", logEntryAsMap(entry)) 234 .disableTracking()); 235 } 236 } 237 } 238 239 private Map<String, Object> logEntryAsMap(LogEntry entry) { 240 Map<String, Object> result = new HashMap<>(); 241 result.put("bundle", 242 Optional 243 .ofNullable(entry.getBundle().getHeaders().get("Bundle-Name")) 244 .orElse(entry.getBundle().getSymbolicName())); 245 result.put("exception", Optional.ofNullable(entry.getException()) 246 .map(exc -> exc.getMessage()).orElse("")); 247 result.put("stacktrace", Optional.ofNullable(entry.getException()) 248 .map(exc -> { 249 ByteArrayOutputStream out = new ByteArrayOutputStream(); 250 PrintWriter pw = new PrintWriter(out); 251 exc.printStackTrace(pw); 252 pw.close(); 253 return out.toString(); 254 }).orElse("")); 255 result.put("location", Optional.ofNullable(entry.getLocation()) 256 .map(loc -> loc.toString()).orElse("")); 257 result.put("loggerName", entry.getLoggerName()); 258 result.put("logLevel", entry.getLogLevel().toString()); 259 result.put("message", entry.getMessage()); 260 result.put("sequence", entry.getSequence()); 261 result.put("service", Optional.ofNullable(entry.getServiceReference()) 262 .map(Object::toString).orElse("")); 263 result.put("threadInfo", entry.getThreadInfo()); 264 result.put("time", entry.getTime()); 265 return result; 266 } 267 268 /* 269 * (non-Javadoc) 270 * 271 * @see org.jgrapes.console.AbstractConlet#doNotifyConletModel 272 */ 273 @Override 274 protected void doNotifyConletModel(NotifyConletModel event, 275 ConsoleSession channel, ConletBaseModel conletState) 276 throws Exception { 277 event.stop(); 278 switch (event.method()) { 279 case "resync": 280 sendAllEntries(channel, event.conletId()); 281 break; 282 } 283 } 284 285 @Override 286 protected boolean doSetLocale(SetLocale event, ConsoleSession channel, 287 String conletId) throws Exception { 288 return true; 289 } 290}