com.aoindustries.aoserv.client.ticket.TicketLoggingHandler Maven / Gradle / Ivy
/*
* aoserv-client - Java client for the AOServ Platform.
* Copyright (C) 2009-2012, 2016, 2017, 2018, 2019, 2020 AO Industries, Inc.
* [email protected]
* 7262 Bull Pen Cir
* Mobile, AL 36695
*
* This file is part of aoserv-client.
*
* aoserv-client is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* aoserv-client is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with aoserv-client. If not, see .
*/
package com.aoindustries.aoserv.client.ticket;
import com.aoindustries.aoserv.client.AOServClientConfiguration;
import com.aoindustries.aoserv.client.AOServConnector;
import com.aoindustries.aoserv.client.account.Account;
import com.aoindustries.aoserv.client.account.User;
import com.aoindustries.aoserv.client.reseller.Brand;
import com.aoindustries.aoserv.client.reseller.Category;
import com.aoindustries.exception.ConfigurationException;
import com.aoindustries.lang.Strings;
import com.aoindustries.util.logging.QueuedHandler;
import com.aoindustries.validation.ValidationException;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
/**
*
* An implementation of {@link Handler} that logs to the ticket system.
* It queues log entries and logs them in the background. The log entries
* are added in the order received, regardless of priority.
*
*
* Will first look for any open/hold/bounced ticket that is for the same
* brand, account, language, type, level, prefix, classname, method, and category.
* If found, it will annotate that ticket. If not found, it will create a new
* ticket.
*
*
* To minimize resource consumption, this shares one {@link ExecutorService} for all handlers,
* which means tickets are fed to the master(s) sequentially, even across many
* different connectors.
*
*
* @author AO Industries, Inc.
*/
public class TicketLoggingHandler extends QueuedHandler {
private static final boolean DEBUG = false;
private static final List> handlers = new ArrayList<>();
/**
* Shares one queue across all handlers.
* This is created when first accessed, and released when the last handler
* is {@linkplain #close() closed}.
*/
private static ExecutorService executor;
private static ExecutorService getExecutor() {
synchronized(handlers) {
if(executor == null) {
executor = Executors.newSingleThreadExecutor(
(Runnable r) -> {
Thread thread = new Thread(r);
thread.setName("Ticket Logger");
thread.setDaemon(true);
thread.setPriority(Thread.NORM_PRIORITY - 1);
return thread;
}
);
}
return executor;
}
}
/**
* Only one TicketLoggingHandler will be created per unique summaryPrefix,
* AOServConnector, and categoryDotPath.
*/
public static TicketLoggingHandler getHandler(String summaryPrefix, AOServConnector connector, String categoryDotPath) {
synchronized(handlers) {
TicketLoggingHandler handler = null;
Iterator> iter = handlers.iterator();
while(iter.hasNext()) {
WeakReference ref = iter.next();
TicketLoggingHandler h = ref.get();
if(h == null) {
// Garbage collected
iter.remove();
} else {
if(
handler == null // Duplicates in list are possible, since the list is added-to by the protected / public constructors, too
&& h.connector == connector
&& Objects.equals(h.summaryPrefix, summaryPrefix)
&& Objects.equals(h.categoryDotPath, categoryDotPath)
) {
handler = h;
}
}
}
if(handler == null) {
handler = new TicketLoggingHandler(
summaryPrefix,
connector,
categoryDotPath
);
}
return handler;
}
}
private final String summaryPrefix;
private final AOServConnector connector;
private final String categoryDotPath;
protected TicketLoggingHandler(String summaryPrefix, AOServConnector connector, String categoryDotPath) {
super(getExecutor());
// super("Ticket logger for " + connector.toString());
synchronized(handlers) {
handlers.add(new WeakReference<>(this));
}
this.summaryPrefix = Strings.nullIfEmpty(summaryPrefix);
this.connector = connector;
this.categoryDotPath = Strings.nullIfEmpty(categoryDotPath);
debug();
}
/**
* Public constructor required so can be specified in logging.properties
.
* Supports the following optional settings in logging.properties
:
*
* (classname).summaryPrefix
- the summary prefix for tickets.
* (classname).username
- the username to login as. When not
* set, the username from aoserv-client.properties
is used.
* (classname).password
- the password to login with. When not
* set, the password from aoserv-client.properties
is used.
* (classname).categoryDotPath
- the {@linkplain Category#getDotPath() category dot path} for tickets.
*
*/
public TicketLoggingHandler() throws ConfigurationException {
super(getExecutor());
synchronized(handlers) {
handlers.add(new WeakReference<>(this));
}
try {
LogManager manager = LogManager.getLogManager();
String cname = getClass().getName();
this.summaryPrefix = Strings.trimNullIfEmpty(
manager.getProperty(cname + ".summaryPrefix")
);
User.Name username = User.Name.valueOf(
Strings.trimNullIfEmpty(
manager.getProperty(cname + ".username")
)
);
if(username == null) username = AOServClientConfiguration.getUsername();
String password = Strings.trimNullIfEmpty(
manager.getProperty(cname + ".password")
);
if(password == null) password = AOServClientConfiguration.getPassword();
this.connector = AOServConnector.getConnector(username, password);
this.categoryDotPath = Strings.trimNullIfEmpty(
manager.getProperty(cname + ".categoryDotPath")
);
} catch(ValidationException e) {
throw new ConfigurationException(e);
}
debug();
}
private void debug() {
if(DEBUG) {
System.err.println();
System.err.println("TicketLoggingHandler: ");
System.err.println(" class...........: " + getClass().getName());
System.err.println(" summaryPrefix...: " + summaryPrefix);
System.err.println(" connector.......: " + connector);
System.err.println(" categoryDotPath.: " + categoryDotPath);
System.err.println();
}
}
/**
* Clean-up this handler and any that were garbage collected.
*/
@Override
public void close() throws SecurityException {
super.close();
synchronized(handlers) {
boolean hasHandler = false;
Iterator> iter = handlers.iterator();
while(iter.hasNext()) {
WeakReference ref = iter.next();
TicketLoggingHandler handler = ref.get();
if(
// Garbage collected
handler == null
// This one
|| handler == this
) {
iter.remove();
} else {
hasHandler = true;
}
}
if(!hasHandler) {
assert handlers.isEmpty();
if(executor != null) {
shutdownExecutor(executor);
executor = null;
}
}
}
}
@Override
protected void backgroundPublish(Formatter formatter, LogRecord record, String fullReport) throws IOException, SQLException {
// Look-up things
Account account = connector.getCurrentAdministrator().getUsername().getPackage().getAccount();
Brand brand = account.getBrand();
if(brand == null) throw new SQLException("Unable to find Brand for connector: " + connector);
Language language = connector.getTicket().getLanguage().get(Language.EN);
if(language == null) throw new SQLException("Unable to find Language: " + Language.EN);
TicketType ticketType = connector.getTicket().getTicketType().get(TicketType.LOGS);
if(ticketType == null) throw new SQLException("Unable to find TicketType: " + TicketType.LOGS);
Category category;
if(categoryDotPath != null) {
category = connector.getReseller().getCategory().getTicketCategoryByDotPath(categoryDotPath);
if(category == null) throw new SQLException("Unable to find Category: " + categoryDotPath);
} else {
category = null;
}
Level level = record.getLevel();
// Generate the summary from level, prefix classname, method
StringBuilder tempSB = new StringBuilder();
tempSB.append('[').append(level).append(']');
if(summaryPrefix != null) tempSB.append(' ').append(summaryPrefix);
tempSB.append(" - ").append(record.getSourceClassName()).append(" - ").append(record.getSourceMethodName());
String summary = tempSB.toString();
// Look for an existing ticket to append
Ticket existingTicket = null;
for(Ticket ticket : connector.getTicket().getTicket()) {
String status = ticket.getStatus().getStatus();
if(
(
Status.OPEN.equals(status)
|| Status.HOLD.equals(status)
|| Status.BOUNCED.equals(status)
) && brand.equals(ticket.getBrand())
&& account.equals(ticket.getAccount())
&& language.equals(ticket.getLanguage())
&& ticketType.equals(ticket.getTicketType())
&& ticket.getSummary().equals(summary) // level, prefix, classname, and method
&& Objects.equals(category, ticket.getCategory())
) {
existingTicket = ticket;
break;
}
}
if(existingTicket != null) {
existingTicket.addAnnotation(
generateActionSummary(formatter, record),
fullReport
);
} else {
// The priority depends on the log level
String priorityName = getPriorityName(level);
Priority priority = connector.getTicket().getPriority().get(priorityName);
if(priority == null) throw new SQLException("Unable to find TicketPriority: " + priorityName);
connector.getTicket().getTicket().addTicket(
brand,
account,
language,
category,
ticketType,
null,
summary,
fullReport,
priority,
Collections.emptySet(),
""
);
}
}
public static String generateActionSummary(Formatter formatter, LogRecord record) {
// Generate the annotation summary as localized message + thrown
StringBuilder tempSB = new StringBuilder();
String message = formatter.formatMessage(record);
if(message != null) {
message = message.trim();
int eol = message.indexOf('\n');
boolean doEllipsis = false;
if(eol != -1) {
message = message.substring(0, eol).trim();
doEllipsis = true;
}
if(message.length()>0) {
tempSB.append(message);
if(doEllipsis) tempSB.append('\u2026');
}
}
Throwable thrown = record.getThrown();
if(thrown != null) {
if(tempSB.length() > 0) tempSB.append(" - ");
String thrownMessage = thrown.getMessage();
boolean doEllipsis = false;
if(thrownMessage != null) {
thrownMessage = thrownMessage.trim();
int eol = thrownMessage.indexOf('\n');
if(eol != -1) {
thrownMessage = thrownMessage.substring(0, eol).trim();
doEllipsis = true;
}
}
if(thrownMessage != null && thrownMessage.length() > 0) {
tempSB.append(thrownMessage);
if(doEllipsis) tempSB.append('\u2026');
} else {
tempSB.append(thrown.toString());
}
}
return tempSB.toString();
}
/**
* Gets the name of a {@link Priority} that corresponds to the given
* {@link Level}.
*/
public static String getPriorityName(Level level) {
String priorityName;
int intLevel = level.intValue();
if (intLevel <= Level.CONFIG .intValue()) priorityName = Priority.LOW; // level <= CONFIG
else if(intLevel <= Level.INFO .intValue()) priorityName = Priority.NORMAL; // CONFIG < level <= INFO
else if(intLevel <= Level.WARNING.intValue()) priorityName = Priority.HIGH; // INFO < level <= WARNING
else priorityName = Priority.URGENT; // WARNING < level
return priorityName;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy