/*
* ALMA - Atacama Large Millimiter Array
* (c) European Southern Observatory, 2004
* Copyright by ESO (in the framework of the ALMA collaboration),
* All rights reserved
*
* This library 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 2.1 of the License, or (at your option) any later version.
*
* This library 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 this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307 USA
*/
package alma.acs.logging;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.LogRecord;
import org.omg.CORBA.Any;
import org.omg.CORBA.ORB;
import org.omg.CORBA.UserException;
import org.omg.DsLogAdmin.Log;
import org.omg.DsLogAdmin.LogOperations;
import alma.Logging.AcsLogServiceOperations;
import alma.Logging.XmlLogRecord;
import alma.acs.logging.formatters.AcsLogFormatter;
import alma.acs.logging.formatters.AcsXMLLogFormatter;
/**
* Sends log records to the remote CORBA log service.
* No caching or threading is done, so this class should be used at the delivering end of a smarter queue.
*
* @author hsommer
* created Apr 19, 2005 4:15:19 PM
*/
class RemoteLogDispatcher {
/**
* Name of the property which will enable using the ACS extensions to the
* telecom logging service ("Log"), mainly to avoid wrapping all log records with a Corba Any.
*/
public static final String USE_ACS_LOGSERVICE_EXTENSIONS_PROPERTYNAME = "alma.acs.logging.useAcsLogServiceExtensions";
public final boolean useAcsLogServiceExtensions = Boolean.getBoolean(USE_ACS_LOGSERVICE_EXTENSIONS_PROPERTYNAME);
/**
* Maximum number of CORBA Anys (with 1 log record each) sent to remote log service at a time.
* This buffer is is configured in the CDB through the attribute <code>dispatchPacketSize</code>.
*/
private int bufferSize = 30;
private final ORB orb;
private final AcsLogServiceOperations logService;
private final AcsLogFormatter logFormatter;
private LogRecordComparator timestampLogRecordComparator;
private final boolean DEBUG = Boolean.getBoolean("alma.acs.logging.verbose");
/**
*
* @param orb used for creation of Anys
* @param logService remote log service to which logs are sent.
* Since the reference is persistent across failures and restarts,
* we can keep working with this object the whole time.
* May be <code>null</code> for unit tests, in which case {@link #sendLogRecords(LogRecord[])}
* will fail and return the all log records inside the <code>FailedLogRecords</code> structure.
* With ACS 7.0.1 we allow the super type {@link LogOperations} instead of the usually
* expected {@link Log}, to allow unit tests with a mock Log service.
* @param logFormatter used to format LogRecords to Any representation.
* No direct assumption on XML is made, so technically any valid Any returned by the formatter will do
* (as far as this class is concerned).
*/
RemoteLogDispatcher(ORB orb, AcsLogServiceOperations logService, AcsLogFormatter logFormatter) {
this.orb = orb;
this.logService = logService;
this.logFormatter = logFormatter;
timestampLogRecordComparator = new LogRecordComparator(true);
if (useAcsLogServiceExtensions && !(logFormatter instanceof AcsXMLLogFormatter)) {
throw new RuntimeException("Only XML-based remote logging is supported with the " + USE_ACS_LOGSERVICE_EXTENSIONS_PROPERTYNAME + " option.");
}
}
public int getBufferSize() {
return bufferSize;
}
/**
* Sets the size of the log record buffer, which determines the maximum number of log records
* that can be sent to the remote log service in one call.
* <p>
* Corresponds to <code>dispatchPacketSize</code> in the CDB.
*
* @param newBuffSize value >=1, otherwise ignored
*/
public void setBufferSize(int newBuffSize) {
if (newBuffSize >= 1) {
bufferSize = newBuffSize;
}
else {
System.out.println("RemoteLogDispatcher#setBufferSize: ignoring illegal value " + newBuffSize);
}
}
/**
* Attempts to send <code>logRecords</code> over to the remote logging service.
* To not lose any log records in case of failure, they can be obtained from the returned
* <code>FailedLogRecords</code> object, and should be fed back to the log record queue in order to try again later.
* <p>
* Should not be called concurrently (which can't happen since we use a single threaded executor
* in <code>DispatchingLogQueue</code>).
* <p>
* Sorts all log records by timestamp before converting them for remote transmission.
*
* @param logRecords
* @return those LogRecords that failed to be sent, either because they could not be converted to Any,
* or because the remote logger failed.
*/
FailedLogRecords sendLogRecords(LogRecord[] logRecords) {
// sort this set of records by timestamp (queue delivers them by level/timestamp)
Arrays.sort(logRecords, timestampLogRecordComparator);
FailedLogRecords failures = new FailedLogRecords();
// used for feeding back these LogRecords if the sending fails
List<LogRecord> candidateLogRecords = new ArrayList<LogRecord>();
if (useAcsLogServiceExtensions) {
// create CORBA XmlLogRecord containing XML representations of log records and the log level as a separate field
List<XmlLogRecord> remoteLogRecords = new ArrayList<XmlLogRecord>();
for (int i = 0; i < logRecords.length; i++) {
if (i < getBufferSize()) {
try {
String xml = ((AcsXMLLogFormatter) logFormatter).format(logRecords[i]);
int level = AcsLogLevel.getNativeLevel(logRecords[i].getLevel()).getAcsLevel().value;
XmlLogRecord remoteLogRecord = new XmlLogRecord(xml, (short) level);
remoteLogRecords.add(remoteLogRecord);
candidateLogRecords.add(logRecords[i]);
}
catch (RuntimeException e) {
failures.addSerializationFailure(logRecords[i]);
}
}
else {
// records that don't fit into one remote call must be sent later
// this should never happen except for during concurrently changing buffer size.
failures.addSendFailure(logRecords[i]);
}
}
// send the log records over CORBA
if (!remoteLogRecords.isEmpty()) {
XmlLogRecord[] remoteLogRecordsArray = remoteLogRecords.toArray(new XmlLogRecord[remoteLogRecords.size()]);
writeRecords(remoteLogRecordsArray);
}
}
else { // default is Corba telecom Log interface that requires records inside Anys
List<Any> anyLogRecords = new ArrayList<Any>();
for (int i = 0; i < logRecords.length; i++) {
if (i < getBufferSize()) {
try {
Any anyLogRecord = orb.create_any();
anyLogRecord = logFormatter.formatAny(anyLogRecord, logRecords[i]);
anyLogRecords.add(anyLogRecord);
candidateLogRecords.add(logRecords[i]);
} catch (RuntimeException e) {
failures.addSerializationFailure(logRecords[i]);
}
}
else {
// records that don't fit into one remote call must be sent later
failures.addSendFailure(logRecords[i]);
}
}
// send the log records over CORBA
if (!anyLogRecords.isEmpty()) {
Any[] anyLogRecordsArray = anyLogRecords.toArray(new Any[anyLogRecords.size()]);
try {
writeRecords(anyLogRecordsArray);
}
// catch (LogFull ex1) {
// // Telecom Log Service spec v. 1.1.2, chapter 1.2.5.1:
// // "if availability status is log_full and LogFullAction is "halt", then a LogFull exception is raised
// // and the number of log records written will be returned in the exception."
// // @TODO: Can we actually assume that this number n refers to the first n log records from the Any[] so that we only feed back the later records?
// int recordsWritten = ex1.n_records_written;
// if (recordsWritten > 0 && recordsWritten < candidateLogRecords.size()) {
// for (int i = recordsWritten; i < candidateLogRecords.size(); i++) {
// failures.addSendFailure(candidateLogRecords.get(i));
// }
// }
// else {
// failures.addSendFailures(candidateLogRecords);
// }
// }
// // @todo: slow down future sending attempts if we get a
// // LogDisabled - operational state "disabled" due to some runtime problem in the Log
// // LogLocked - hopefully temporary admin setting on the Log
// // LogOffDuty - other exotic reasons to be not "on duty", such as log duration time or scheduling time
catch (Throwable thr) {
// feed back these records to we can try them again later
failures.addSendFailures(candidateLogRecords);
// Currently ACS does not make use of the semantics of the various exceptions specified here by CORBA, i.e.
// LogDisabled, LogOffDuty, LogLocked, LogFull (see above)
// There can also be java.net.ConnectException thrown.. @TODO perhaps we should print that to stdout, with repeatGuard.
}
}
}
return failures;
}
/**
* The CORBA call to {@link Log#write_records(Any[])}. May be faked by test subclasses.
*
* @param anyLogRecordsArray
* @throws UserException
*/
protected void writeRecords(Any[] anyLogRecordsArray) throws UserException {
logService.write_records(anyLogRecordsArray);
if (DEBUG) {
System.out.println("sent the following Any log records to the log service:");
for (int i = 0; i < anyLogRecordsArray.length; i++) {
// TODO: CARLI, change that here assumes that is string, it can be a binary log!!!
System.out.println("" + i + ") " + anyLogRecordsArray[i].extract_string());
}
}
}
/**
* Alternative to {@link #writeRecords(Any[])} used when property {@code alma.acs.logging.useAcsLogServiceExtensions} is set.
* @param remoteLogRecords
*/
protected void writeRecords(XmlLogRecord[] remoteLogRecords) {
logService.writeRecords(remoteLogRecords);
}
static class FailedLogRecords
{
private List<LogRecord> serializeFailures;
private List<LogRecord> sendFailures;
void addSerializationFailure(LogRecord logRecord) {
if (serializeFailures == null) {
serializeFailures = new ArrayList<LogRecord>();
}
serializeFailures.add(logRecord);
}
void addSendFailure(LogRecord logRecord) {
if (sendFailures == null) {
sendFailures = new ArrayList<LogRecord>();
}
sendFailures.add(logRecord);
}
void addSendFailures(List<LogRecord> logRecords) {
if (sendFailures == null) {
sendFailures = new ArrayList<LogRecord>();
}
sendFailures.addAll(logRecords);
}
boolean hasSerializationFailures() {
return (serializeFailures != null);
}
List<LogRecord> getSerializationFailures() {
if (serializeFailures == null) {
return new ArrayList<LogRecord>(0);
} else {
return serializeFailures;
}
}
boolean hasSendFailures() {
return (sendFailures != null);
}
List<LogRecord> getSendFailures() {
if (sendFailures == null) {
return new ArrayList<LogRecord>(0);
} else {
return sendFailures;
}
}
}
}