/* * JBoss, Home of Professional Open Source. * Copyright 2012, 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.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM; import java.io.BufferedReader; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.jboss.as.controller.AttributeDefinition; import org.jboss.as.controller.ModelVersion; 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.PathAddress; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.ReloadRequiredRemoveStepHandler; import org.jboss.as.controller.ReloadRequiredWriteAttributeHandler; 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.operations.validation.IntRangeValidator; import org.jboss.as.controller.operations.validation.StringLengthValidator; import org.jboss.as.controller.registry.AttributeAccess.Flag; import org.jboss.as.controller.registry.ManagementResourceRegistration; import org.jboss.as.controller.registry.Resource; import org.jboss.as.controller.services.path.PathManager; import org.jboss.as.controller.transform.description.DiscardAttributeChecker; import org.jboss.as.controller.transform.description.RejectAttributeChecker; import org.jboss.as.controller.transform.description.ResourceTransformationDescriptionBuilder; import org.jboss.as.logging.logging.LoggingLogger; import org.jboss.as.logging.logmanager.WildFlyLogContextSelector; import org.jboss.as.server.ServerEnvironment; import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelType; import org.jboss.dmr.Property; /** * @author <a href="mailto:tomaz.cerar@redhat.com">Tomaz Cerar</a> * @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a> */ public class LoggingResourceDefinition extends TransformerResourceDefinition { static final PathElement SUBSYSTEM_PATH = PathElement.pathElement(SUBSYSTEM, LoggingExtension.SUBSYSTEM_NAME); static final SimpleAttributeDefinition ADD_LOGGING_API_DEPENDENCIES = SimpleAttributeDefinitionBuilder.create("add-logging-api-dependencies", ModelType.BOOLEAN, true) .setAllowExpression(true) .setAttributeMarshaller(ElementAttributeMarshaller.VALUE_ATTRIBUTE_MARSHALLER) .setDefaultValue(new ModelNode(true)) .setFlags(Flag.RESTART_ALL_SERVICES) .build(); static final SimpleAttributeDefinition USE_DEPLOYMENT_LOGGING_CONFIG = SimpleAttributeDefinitionBuilder.create("use-deployment-logging-config", ModelType.BOOLEAN, true) .setAllowExpression(true) .setAttributeMarshaller(ElementAttributeMarshaller.VALUE_ATTRIBUTE_MARSHALLER) .setDefaultValue(new ModelNode(true)) .setFlags(Flag.RESTART_ALL_SERVICES) .build(); static final SimpleAttributeDefinition NAME = SimpleAttributeDefinitionBuilder.create("name", ModelType.STRING, false) .setAllowExpression(true) .setValidator(new StringLengthValidator(1, false)) .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 SimpleAttributeDefinition FILE_NAME = SimpleAttributeDefinitionBuilder.create("file-name", ModelType.STRING, false) .setAllowExpression(false) .build(); static final SimpleAttributeDefinition FILE_SIZE = SimpleAttributeDefinitionBuilder.create("file-size", ModelType.LONG, false) .setAllowExpression(false) .build(); static final SimpleAttributeDefinition LAST_MODIFIED_DATE = SimpleAttributeDefinitionBuilder.create("last-modified-date", ModelType.STRING, false) .setAllowExpression(false) .build(); static final SimpleOperationDefinition READ_LOG_FILE = new SimpleOperationDefinitionBuilder("read-log-file", LoggingExtension.getResourceDescriptionResolver()) .addAccessConstraint(LogFileResourceDefinition.VIEW_SERVER_LOGS) .setDeprecated(ModelVersion.create(3, 0, 0)) .setParameters(NAME, CommonAttributes.ENCODING, LINES, SKIP, TAIL) .setReplyType(ModelType.LIST) .setReplyValueType(ModelType.STRING) .setReadOnly() .setRuntimeOnly() .build(); static final SimpleOperationDefinition LIST_LOG_FILES = new SimpleOperationDefinitionBuilder("list-log-files", LoggingExtension.getResourceDescriptionResolver()) .addAccessConstraint(LogFileResourceDefinition.VIEW_SERVER_LOGS) .setDeprecated(ModelVersion.create(3, 0, 0)) .setReplyType(ModelType.LIST) .setReplyParameters(FILE_NAME, FILE_SIZE, LAST_MODIFIED_DATE) .setReadOnly() .setRuntimeOnly() .build(); static final SimpleAttributeDefinition[] ATTRIBUTES = { ADD_LOGGING_API_DEPENDENCIES, USE_DEPLOYMENT_LOGGING_CONFIG, }; private final PathManager pathManager; protected LoggingResourceDefinition(final PathManager pathManager, final WildFlyLogContextSelector contextSelector) { super(SUBSYSTEM_PATH, LoggingExtension.getResourceDescriptionResolver(), new LoggingSubsystemAdd(pathManager, contextSelector), ReloadRequiredRemoveStepHandler.INSTANCE); this.pathManager = pathManager; } @Override public void registerAttributes(final ManagementResourceRegistration resourceRegistration) { super.registerAttributes(resourceRegistration); final OperationStepHandler writeHandler = new ReloadRequiredWriteAttributeHandler(ATTRIBUTES); for (SimpleAttributeDefinition attribute : ATTRIBUTES) { resourceRegistration.registerReadWriteAttribute(attribute, null, writeHandler); } } @Override public void registerOperations(final ManagementResourceRegistration resourceRegistration) { super.registerOperations(resourceRegistration); // Only register on server if (pathManager != null) { resourceRegistration.registerOperationHandler(LIST_LOG_FILES, new ListLogFilesOperation()); resourceRegistration.registerOperationHandler(READ_LOG_FILE, new ReadLogFileOperation()); } } @Override public void registerTransformers(final KnownModelVersion modelVersion, final ResourceTransformationDescriptionBuilder rootResourceBuilder, final ResourceTransformationDescriptionBuilder loggingProfileBuilder) { switch (modelVersion) { case VERSION_1_3_0: { rootResourceBuilder.getAttributeBuilder() .setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(false, true, ADD_LOGGING_API_DEPENDENCIES.getDefaultValue()), ADD_LOGGING_API_DEPENDENCIES) .addRejectCheck(RejectAttributeChecker.DEFINED, ADD_LOGGING_API_DEPENDENCIES) .end(); break; } case VERSION_1_5_0: { rootResourceBuilder.getAttributeBuilder() .setDiscard(new DiscardAttributeChecker.DiscardAttributeValueChecker(false, true, USE_DEPLOYMENT_LOGGING_CONFIG.getDefaultValue()), USE_DEPLOYMENT_LOGGING_CONFIG) .addRejectCheck(RejectAttributeChecker.DEFINED, USE_DEPLOYMENT_LOGGING_CONFIG) .end(); break; } } } private class ListLogFilesOperation implements OperationStepHandler { @Override public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException { final ModelNode model = Resource.Tools.readModel(context.readResource(PathAddress.EMPTY_ADDRESS)); final String logDir = pathManager.getPathEntry(ServerEnvironment.SERVER_LOG_DIR).resolvePath(); List<File> logFiles; try { logFiles = findFiles(logDir, model); } catch (IOException e) { logFiles = Collections.emptyList(); LoggingLogger.ROOT_LOGGER.errorProcessingLogDirectory(logDir); } final SimpleDateFormat dateFormat = new SimpleDateFormat(LogFileResourceDefinition.ISO_8601_FORMAT); final ModelNode result = context.getResult().setEmptyList(); for (File logFile : logFiles) { final ModelNode fileInfo = new ModelNode(); fileInfo.get(FILE_NAME.getName()).set(logFile.getName()); fileInfo.get(FILE_SIZE.getName()).set(logFile.length()); fileInfo.get(LAST_MODIFIED_DATE.getName()).set(dateFormat.format(new Date(logFile.lastModified()))); result.add(fileInfo); } context.completeStep(ResultHandler.NOOP_RESULT_HANDLER); } } /** * 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 */ private class ReadLogFileOperation implements OperationStepHandler { @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 String fileName = NAME.resolveModelAttribute(context, operation).asString(); 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 = CommonAttributes.ENCODING.resolveModelAttribute(context, operation); final String encoding = (encodingModel.isDefined() ? encodingModel.asString() : null); 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); } final ModelNode model = Resource.Tools.readModel(context.readResource(PathAddress.EMPTY_ADDRESS)); final List<File> logFiles; try { logFiles = findFiles(pathManager.getPathEntry(ServerEnvironment.SERVER_LOG_DIR).resolvePath(), model); } catch (IOException e) { throw LoggingLogger.ROOT_LOGGER.failedToReadLogFile(e, fileName); } // User must have permissions to read the file if (!path.canRead() || !logFiles.contains(path)) { throw LoggingLogger.ROOT_LOGGER.readNotAllowed(fileName); } // 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<String>(); } else { lines = new ArrayList<String>(numberOfLines); } final InputStream in; BufferedReader reader = null; try { if (tail) { in = new LogFileResourceDefinition.LifoFileInputStream(file); } else { in = new FileInputStream(file); } if (encoding == null) { //system default used on purpose reader = new BufferedReader(new InputStreamReader(in)); } else { reader = new BufferedReader(new InputStreamReader(in, encoding)); } 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; } finally { safeClose(reader); } } } private static void safeClose(final Closeable closeable) { if (closeable != null) try { closeable.close(); } catch (Throwable t) { LoggingLogger.ROOT_LOGGER.failedToCloseResource(t, closeable); } } private static List<File> findFiles(final String defaultLogDir, final ModelNode model) throws IOException { final Set<Path> files = LoggingResource.findFiles(defaultLogDir, model, false); // Also need to include logging profile log files if (model.hasDefined(CommonAttributes.LOGGING_PROFILE)) { for (Property property : model.get(CommonAttributes.LOGGING_PROFILE).asPropertyList()) { files.addAll(LoggingResource.findFiles(defaultLogDir, property.getValue(), false)); } } return files.stream() .map(Path::toFile) .collect(Collectors.toCollection(ArrayList::new)); } }