/* * JBoss, Home of Professional Open Source. * Copyright 2014, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This 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 software 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 software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.logging; import static org.jboss.as.logging.CommonAttributes.ENCODING; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.RandomAccessFile; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import org.jboss.as.controller.AttributeDefinition; import org.jboss.as.controller.OperationContext; import org.jboss.as.controller.OperationContext.ResultHandler; import org.jboss.as.controller.OperationFailedException; import org.jboss.as.controller.OperationStepHandler; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.SimpleAttributeDefinition; import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; import org.jboss.as.controller.SimpleOperationDefinition; import org.jboss.as.controller.SimpleOperationDefinitionBuilder; import org.jboss.as.controller.SimpleResourceDefinition; import org.jboss.as.controller.access.constraint.SensitivityClassification; import org.jboss.as.controller.access.management.AccessConstraintDefinition; import org.jboss.as.controller.access.management.SensitiveTargetAccessConstraintDefinition; import org.jboss.as.controller.operations.validation.IntRangeValidator; import org.jboss.as.controller.registry.ManagementResourceRegistration; import org.jboss.as.controller.services.path.PathManager; import org.jboss.as.logging.logging.LoggingLogger; import org.jboss.as.server.ServerEnvironment; import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelType; /** * @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a> */ class LogFileResourceDefinition extends SimpleResourceDefinition { static final AccessConstraintDefinition VIEW_SERVER_LOGS = new SensitiveTargetAccessConstraintDefinition( new SensitivityClassification(LoggingExtension.SUBSYSTEM_NAME, "view-server-logs", false, false, false)); static final String LOG_FILE = "log-file"; static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; static final SimpleAttributeDefinition FILE_SIZE = SimpleAttributeDefinitionBuilder.create("file-size", ModelType.LONG, false) .setStorageRuntime() .setAllowExpression(false) .build(); static final SimpleAttributeDefinition LAST_MODIFIED_TIME = SimpleAttributeDefinitionBuilder.create("last-modified-time", ModelType.LONG, false) .setStorageRuntime() .setAllowExpression(false) .build(); static final SimpleAttributeDefinition LAST_MODIFIED_TIMESTAMP = SimpleAttributeDefinitionBuilder.create("last-modified-timestamp", ModelType.STRING, false) .setStorageRuntime() .setAllowExpression(false) .build(); static final SimpleAttributeDefinition STREAM = SimpleAttributeDefinitionBuilder.create("stream", ModelType.STRING) .setStorageRuntime() .setAllowNull(true) .build(); static final SimpleAttributeDefinition LINES = SimpleAttributeDefinitionBuilder.create("lines", ModelType.INT, true) .setAllowExpression(true) .setDefaultValue(new ModelNode(10)) .setValidator(new IntRangeValidator(-1, true)) .build(); static final SimpleAttributeDefinition SKIP = SimpleAttributeDefinitionBuilder.create("skip", ModelType.INT, true) .setAllowExpression(true) .setDefaultValue(new ModelNode(0)) .setValidator(new IntRangeValidator(0, true)) .build(); static final SimpleAttributeDefinition TAIL = SimpleAttributeDefinitionBuilder.create("tail", ModelType.BOOLEAN, true) .setAllowExpression(true) .setDefaultValue(new ModelNode(true)) .build(); static final SimpleOperationDefinition READ_LOG_FILE = new SimpleOperationDefinitionBuilder("read-log-file", LoggingExtension.getResourceDescriptionResolver()) .addAccessConstraint(VIEW_SERVER_LOGS) .setParameters(ENCODING, LINES, SKIP, TAIL) .setReplyType(ModelType.LIST) .setReplyValueType(ModelType.STRING) .setReadOnly() .setRuntimeOnly() .build(); static final PathElement LOG_FILE_PATH = PathElement.pathElement("log-file"); private final PathManager pathManager; protected LogFileResourceDefinition(final PathManager pathManager) { super(new Parameters(LOG_FILE_PATH, LoggingExtension.getResourceDescriptionResolver("log-file")) .setRuntime().setAccessConstraints(VIEW_SERVER_LOGS)); assert pathManager != null : "PathManager cannot be null"; this.pathManager = pathManager; } @Override public void registerOperations(final ManagementResourceRegistration resourceRegistration) { super.registerOperations(resourceRegistration); resourceRegistration.registerOperationHandler(READ_LOG_FILE, new ReadLogFileOperation(pathManager)); } @Override public void registerAttributes(final ManagementResourceRegistration resourceRegistration) { super.registerAttributes(resourceRegistration); resourceRegistration.registerReadOnlyAttribute(FILE_SIZE, new ReadAttributeOperationStepHandler() { @Override protected void updateModel(final Path path, final ModelNode model) throws IOException { model.set(Files.size(path)); } }); resourceRegistration.registerReadOnlyAttribute(LAST_MODIFIED_TIME, new ReadAttributeOperationStepHandler() { @Override protected void updateModel(final Path path, final ModelNode model) throws IOException { model.set(Files.getLastModifiedTime(path).toMillis()); } }); resourceRegistration.registerReadOnlyAttribute(LAST_MODIFIED_TIMESTAMP, new ReadAttributeOperationStepHandler() { @Override protected void updateModel(final Path path, final ModelNode model) throws IOException { final SimpleDateFormat sdf = new SimpleDateFormat(ISO_8601_FORMAT); model.set(sdf.format(new Date(Files.getLastModifiedTime(path).toMillis()))); } }); final OperationStepHandler streamHandler = new OperationStepHandler() { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { final Path path = Paths.get(pathManager.resolveRelativePathEntry(LoggingOperations.getAddressName(operation), ServerEnvironment.SERVER_LOG_DIR)); try { String uuid = context.attachResultStream("text/plain", Files.newInputStream(path)); context.getResult().set(uuid); } catch (IOException e) { throw new RuntimeException(e); } } }; resourceRegistration.registerReadOnlyAttribute(STREAM, streamHandler); } private abstract class ReadAttributeOperationStepHandler implements OperationStepHandler { @Override public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException { final ModelNode model = context.getResult(); final String name = LoggingOperations.getAddressName(operation); final String logDir = pathManager.getPathEntry(ServerEnvironment.SERVER_LOG_DIR).resolvePath(); final Path path = Paths.get(logDir, name); if (Files.notExists(path)) { throw LoggingLogger.ROOT_LOGGER.logFileNotFound(name, logDir); } try { updateModel(path, model); } catch (IOException e) { throw new RuntimeException(e); } context.completeStep(ResultHandler.NOOP_RESULT_HANDLER); } protected abstract void updateModel(Path path, ModelNode model) throws IOException; } /** * Reads a log file and returns the results. * <p/> * <i>Note: </i> If this operation ends up being repeatedly invoked, from the web console for instance, there could * be a performance impact as the model is read and processed for file names during each invocation */ static class ReadLogFileOperation implements OperationStepHandler { private final PathManager pathManager; private ReadLogFileOperation(final PathManager pathManager) { this.pathManager = pathManager; } @Override public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException { // Validate the operation for (AttributeDefinition attribute : READ_LOG_FILE.getParameters()) { attribute.validateOperation(operation); } final int numberOfLines = LINES.resolveModelAttribute(context, operation).asInt(); final int skip = SKIP.resolveModelAttribute(context, operation).asInt(); final boolean tail = TAIL.resolveModelAttribute(context, operation).asBoolean(); final ModelNode encodingModel = ENCODING.resolveModelAttribute(context, operation); final String encoding = (encodingModel.isDefined() ? encodingModel.asString() : null); final String fileName = LoggingOperations.getAddressName(operation); final File path = new File(pathManager.resolveRelativePathEntry(fileName, ServerEnvironment.SERVER_LOG_DIR)); // The file must exist if (!path.exists()) { throw LoggingLogger.ROOT_LOGGER.logFileNotFound(fileName, ServerEnvironment.SERVER_LOG_DIR); } // Read the contents of the log file try { final List<String> lines; if (numberOfLines == 0) { lines = Collections.emptyList(); } else { lines = readLines(path, encoding, tail, skip, numberOfLines); } final ModelNode result = context.getResult().setEmptyList(); for (String line : lines) { result.add(line); } } catch (IOException e) { throw LoggingLogger.ROOT_LOGGER.failedToReadLogFile(e, fileName); } context.completeStep(ResultHandler.NOOP_RESULT_HANDLER); } private List<String> readLines(final File file, final String encoding, final boolean tail, final int skip, final int numberOfLines) throws IOException { final List<String> lines; if (numberOfLines < 0) { lines = new ArrayList<>(); } else { lines = new ArrayList<>(numberOfLines); } try ( final InputStream in = (tail ? new LifoFileInputStream(file) : Files.newInputStream(file.toPath())); /* we should stick with the default here and not use UTF-8. The encoding on the file handler does not default to UTF-8 but the system default. I think here we should stick with the system default unless explicitly defined. I could see a UTF-8 default possibly being problematic on IBM bases systems. */ final InputStreamReader isr = (encoding == null ? new InputStreamReader(in) : new InputStreamReader(in, encoding)); final BufferedReader reader = new BufferedReader(isr) ) { int lineCount = 0; String line; while ((line = reader.readLine()) != null) { if (++lineCount <= skip) continue; if (lines.size() == numberOfLines) break; lines.add(line); } if (tail) { Collections.reverse(lines); } return lines; } } } static final class LifoFileInputStream extends InputStream { private final RandomAccessFile raf; private final long len; private long start; private long end; private long pos; LifoFileInputStream(final File file) throws IOException { raf = new RandomAccessFile(file, "r"); len = raf.length(); start = len; end = len; pos = end; } private void positionFile() throws IOException { end = start; // If we're at the beginning of the file, nothing more to read if (end == 0) { end = -1; start = -1; pos = -1; return; } long filePointer = start - 1; while (true) { filePointer--; // We're at the start of the file if (filePointer < 0) { break; } // Position the file raf.seek(filePointer); final byte readByte = raf.readByte(); // If the byte is a line feed we've found the next line ignoring the last line feed in the file if (readByte == '\n' && filePointer != (len - 1)) { break; } } start = filePointer + 1; pos = start; } @Override public int read() throws IOException { if (pos < end) { raf.seek(pos++); return raf.readByte(); } else if (pos < 0) { return -1; } else { positionFile(); return read(); } } @Override public void close() throws IOException { raf.close(); } } }