diff options
author | Alex Rudyy <orudyy@apache.org> | 2013-08-02 17:03:34 +0000 |
---|---|---|
committer | Alex Rudyy <orudyy@apache.org> | 2013-08-02 17:03:34 +0000 |
commit | 6968f5202decb4c2ecacab61c00552b0515cc805 (patch) | |
tree | c796e7359daec0129ed04dc7f7012ce471870b2a | |
parent | 9963e6fed203515ab3be76fa0e7eebeb63f78583 (diff) | |
download | qpid-python-6968f5202decb4c2ecacab61c00552b0515cc805.tar.gz |
QPID-5037: Move log viewer into a separate tab and add abilities to download logs and filter log entries in the logs table
git-svn-id: https://svn.apache.org/repos/asf/qpid/trunk/qpid@1509778 13f79535-47bb-0310-9956-ffa450edef68
39 files changed, 2911 insertions, 62 deletions
diff --git a/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java index d87a1755da..e66680ce12 100644 --- a/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java +++ b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java @@ -20,7 +20,6 @@ */ package org.apache.qpid.server.management.plugin; -import java.io.File; import java.lang.reflect.Type; import java.net.SocketAddress; import java.util.Collection; @@ -39,7 +38,9 @@ import org.apache.qpid.server.management.plugin.filter.ForbiddingAuthorisationFi import org.apache.qpid.server.management.plugin.filter.RedirectingAuthorisationFilter; import org.apache.qpid.server.management.plugin.servlet.DefinedFileServlet; import org.apache.qpid.server.management.plugin.servlet.FileServlet; +import org.apache.qpid.server.management.plugin.servlet.LogFileServlet; import org.apache.qpid.server.management.plugin.servlet.rest.HelperServlet; +import org.apache.qpid.server.management.plugin.servlet.rest.LogFileListingServlet; import org.apache.qpid.server.management.plugin.servlet.rest.LogRecordsServlet; import org.apache.qpid.server.management.plugin.servlet.rest.LogoutServlet; import org.apache.qpid.server.management.plugin.servlet.rest.MessageContentServlet; @@ -312,6 +313,8 @@ public class HttpManagement extends AbstractPluginAdapter implements HttpManagem root.addServlet(new ServletHolder(FileServlet.INSTANCE), "*.txt"); root.addServlet(new ServletHolder(FileServlet.INSTANCE), "*.xsl"); root.addServlet(new ServletHolder(new HelperServlet()), "/rest/helper"); + root.addServlet(new ServletHolder(new LogFileListingServlet()), "/rest/logfiles"); + root.addServlet(new ServletHolder(new LogFileServlet()), "/rest/logfile"); final SessionManager sessionManager = root.getSessionHandler().getSessionManager(); sessionManager.setSessionCookie(JSESSIONID_COOKIE_PREFIX + lastPort); diff --git a/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/log/LogFileDetails.java b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/log/LogFileDetails.java new file mode 100644 index 0000000000..09dabd0e73 --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/log/LogFileDetails.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.qpid.server.management.plugin.log; + +import java.io.File; + +public class LogFileDetails +{ + private String _name; + private File _location; + private String _mimeType; + private long _size; + private long _lastModified; + private String _appenderName; + + public LogFileDetails(String name, String appenderName, File location, String mimeType, long fileSize, long lastUpdateTime) + { + super(); + _name = name; + _location = location; + _mimeType = mimeType; + _size = fileSize; + _lastModified = lastUpdateTime; + _appenderName = appenderName; + } + + public String getName() + { + return _name; + } + + public File getLocation() + { + return _location; + } + + public String getMimeType() + { + return _mimeType; + } + + public long getSize() + { + return _size; + } + + public long getLastModified() + { + return _lastModified; + } + + public String getAppenderName() + { + return _appenderName; + } + + @Override + public String toString() + { + return "LogFileDetails [name=" + _name + "]"; + } + +} diff --git a/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/log/LogFileHelper.java b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/log/LogFileHelper.java new file mode 100644 index 0000000000..03d98d020b --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/log/LogFileHelper.java @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.qpid.server.management.plugin.log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.apache.log4j.Appender; +import org.apache.log4j.FileAppender; +import org.apache.log4j.QpidCompositeRollingAppender; + +public class LogFileHelper +{ + public static final String GZIP_MIME_TYPE = "application/x-gzip"; + public static final String TEXT_MIME_TYPE = "text/plain"; + public static final String ZIP_MIME_TYPE = "application/zip"; + public static final String GZIP_EXTENSION = ".gz"; + private static final int BUFFER_LENGTH = 1024 * 4; + private Collection<Appender> _appenders; + + public LogFileHelper(Collection<Appender> appenders) + { + super(); + _appenders = appenders; + } + + public List<LogFileDetails> findLogFileDetails(String[] requestedFiles) + { + List<LogFileDetails> logFiles = new ArrayList<LogFileDetails>(); + Map<String, List<LogFileDetails>> cache = new HashMap<String, List<LogFileDetails>>(); + for (int i = 0; i < requestedFiles.length; i++) + { + String[] paths = requestedFiles[i].split("/"); + if (paths.length != 2) + { + throw new IllegalArgumentException("Log file name '" + requestedFiles[i] + "' does not include an appender name"); + } + + String appenderName = paths[0]; + String fileName = paths[1]; + + List<LogFileDetails> appenderFiles = cache.get(appenderName); + if (appenderFiles == null) + { + Appender fileAppender = null; + for (Appender appender : _appenders) + { + if (appenderName.equals(appender.getName())) + { + fileAppender = appender; + break; + } + } + if (fileAppender == null) + { + continue; + } + appenderFiles = getAppenderFiles(fileAppender, true); + if (appenderFiles == null) + { + continue; + } + cache.put(appenderName, appenderFiles); + } + for (LogFileDetails logFileDetails : appenderFiles) + { + if (logFileDetails.getName().equals(fileName)) + { + logFiles.add(logFileDetails); + } + } + } + return logFiles; + } + + public List<LogFileDetails> getLogFileDetails(boolean includeLogFileLocation) + { + List<LogFileDetails> results = new ArrayList<LogFileDetails>(); + for (Appender appender : _appenders) + { + List<LogFileDetails> appenderFiles = getAppenderFiles(appender, includeLogFileLocation); + if (appenderFiles != null) + { + results.addAll(appenderFiles); + } + } + return results; + } + + public void writeLogFiles(List<LogFileDetails> logFiles, OutputStream os) throws IOException + { + ZipOutputStream out = new ZipOutputStream(os); + try + { + addLogFileEntries(logFiles, out); + } + finally + { + out.close(); + } + } + + public void writeLogFile(File file, OutputStream os) throws IOException + { + FileInputStream fis = new FileInputStream(file); + try + { + byte[] bytes = new byte[BUFFER_LENGTH]; + int length = 1; + while ((length = fis.read(bytes)) != -1) + { + os.write(bytes, 0, length); + } + } + finally + { + fis.close(); + } + } + + private List<LogFileDetails> getAppenderFiles(Appender appender, boolean includeLogFileLocation) + { + if (appender instanceof QpidCompositeRollingAppender) + { + return listAppenderFiles((QpidCompositeRollingAppender) appender, includeLogFileLocation); + } + else if (appender instanceof FileAppender) + { + return listAppenderFiles((FileAppender) appender, includeLogFileLocation); + } + return null; + } + + private List<LogFileDetails> listAppenderFiles(FileAppender appender, boolean includeLogFileLocation) + { + String appenderFilePath = appender.getFile(); + File appenderFile = new File(appenderFilePath); + if (appenderFile.exists()) + { + return listLogFiles(appenderFile.getParentFile(), appenderFile.getName(), appender.getName(), "", includeLogFileLocation); + } + return Collections.emptyList(); + } + + private List<LogFileDetails> listAppenderFiles(QpidCompositeRollingAppender appender, boolean includeLogFileLocation) + { + List<LogFileDetails> files = listAppenderFiles((FileAppender) appender, includeLogFileLocation); + String appenderFilePath = appender.getFile(); + File appenderFile = new File(appenderFilePath); + File backupFolder = new File(appender.getBackupFilesToPath()); + if (backupFolder.exists()) + { + String backFolderName = backupFolder.getName() + "/"; + List<LogFileDetails> backedUpFiles = listLogFiles(backupFolder, appenderFile.getName(), appender.getName(), + backFolderName, includeLogFileLocation); + files.addAll(backedUpFiles); + } + return files; + } + + private List<LogFileDetails> listLogFiles(File parent, String baseFileName, String appenderName, String relativePath, + boolean includeLogFileLocation) + { + List<LogFileDetails> files = new ArrayList<LogFileDetails>(); + for (File file : parent.listFiles()) + { + String name = file.getName(); + if (name.startsWith(baseFileName)) + { + files.add(new LogFileDetails(name, appenderName, includeLogFileLocation ? file : null, getMimeType(name), file.length(), + file.lastModified())); + } + } + return files; + } + + private String getMimeType(String fileName) + { + if (fileName.endsWith(GZIP_EXTENSION)) + { + return GZIP_MIME_TYPE; + } + return TEXT_MIME_TYPE; + } + + private void addLogFileEntries(List<LogFileDetails> files, ZipOutputStream out) throws IOException + { + for (LogFileDetails logFileDetails : files) + { + File file = logFileDetails.getLocation(); + if (file.exists()) + { + ZipEntry entry = new ZipEntry(logFileDetails.getAppenderName() + "/" + logFileDetails.getName()); + entry.setSize(file.length()); + out.putNextEntry(entry); + writeLogFile(file, out); + out.closeEntry(); + } + out.flush(); + } + } + +} diff --git a/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/LogFileServlet.java b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/LogFileServlet.java new file mode 100644 index 0000000000..1fa03dc3dc --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/LogFileServlet.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.qpid.server.management.plugin.servlet; + +import java.io.IOException; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.log4j.LogManager; +import org.apache.qpid.server.management.plugin.log.LogFileDetails; +import org.apache.qpid.server.management.plugin.log.LogFileHelper; +import org.apache.qpid.server.management.plugin.servlet.rest.AbstractServlet; + +public class LogFileServlet extends AbstractServlet +{ + private static final long serialVersionUID = 1L; + + public static final String LOGS_FILE_NAME = "qpid-logs-%s.zip"; + public static final String DATE_FORMAT = "yyyy-MM-dd-mmHHss"; + + @SuppressWarnings("unchecked") + private LogFileHelper _helper = new LogFileHelper(Collections.list(LogManager.getRootLogger().getAllAppenders())); + + @Override + protected void doGetWithSubjectAndActor(HttpServletRequest request, HttpServletResponse response) throws IOException, + ServletException + { + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0); + + if (!getBroker().getSecurityManager().authoriseLogsAccess()) + { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Log files download is denied"); + return; + } + + String[] requestedFiles = request.getParameterValues("l"); + + if (requestedFiles == null || requestedFiles.length == 0) + { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + List<LogFileDetails> logFiles = null; + + try + { + logFiles = _helper.findLogFileDetails(requestedFiles); + } + catch(IllegalArgumentException e) + { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + if (logFiles.size() == 0) + { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + String fileName = String.format(LOGS_FILE_NAME, new SimpleDateFormat(DATE_FORMAT).format(new Date())); + response.setStatus(HttpServletResponse.SC_OK); + response.setHeader("Content-Disposition", "attachment;filename=" + fileName); + response.setContentType(LogFileHelper.ZIP_MIME_TYPE); + + OutputStream os = response.getOutputStream(); + try + { + _helper.writeLogFiles(logFiles, os); + } + finally + { + if (os != null) + { + os.close(); + } + } + } + +} diff --git a/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/LogFileListingServlet.java b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/LogFileListingServlet.java new file mode 100644 index 0000000000..b6face18e3 --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/LogFileListingServlet.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.qpid.server.management.plugin.servlet.rest; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.log4j.LogManager; +import org.apache.qpid.server.management.plugin.log.LogFileDetails; +import org.apache.qpid.server.management.plugin.log.LogFileHelper; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializationConfig; + +public class LogFileListingServlet extends AbstractServlet +{ + private static final long serialVersionUID = 1L; + + @SuppressWarnings("unchecked") + private LogFileHelper _helper = new LogFileHelper(Collections.list(LogManager.getRootLogger().getAllAppenders())); + + @Override + protected void doGetWithSubjectAndActor(HttpServletRequest request, HttpServletResponse response) throws IOException, + ServletException + { + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0); + + if (!getBroker().getSecurityManager().authoriseLogsAccess()) + { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Log files download is denied"); + return; + } + + List<LogFileDetails> logFiles = _helper.getLogFileDetails(false); + response.setContentType("application/json"); + response.setStatus(HttpServletResponse.SC_OK); + + final PrintWriter writer = response.getWriter(); + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true); + mapper.writeValue(writer, logFiles); + + response.setStatus(HttpServletResponse.SC_OK); + } + +} diff --git a/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/LogRecordsServlet.java b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/LogRecordsServlet.java index f2cf5d7734..35523ddf0f 100644 --- a/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/LogRecordsServlet.java +++ b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/LogRecordsServlet.java @@ -31,6 +31,10 @@ import org.codehaus.jackson.map.SerializationConfig; public class LogRecordsServlet extends AbstractServlet { + private static final long serialVersionUID = 2L; + + public static final String PARAM_LAST_LOG_ID = "lastLogId"; + public LogRecordsServlet() { super(); @@ -46,12 +50,31 @@ public class LogRecordsServlet extends AbstractServlet response.setHeader("Pragma","no-cache"); response.setDateHeader ("Expires", 0); + if (!getBroker().getSecurityManager().authoriseLogsAccess()) + { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Broker logs access is denied"); + return; + } + + long lastLogId = 0; + try + { + lastLogId = Long.parseLong(request.getParameter(PARAM_LAST_LOG_ID)); + } + catch(Exception e) + { + // ignore null and incorrect parameter values + } + List<Map<String,Object>> logRecords = new ArrayList<Map<String, Object>>(); LogRecorder logRecorder = getBroker().getLogRecorder(); for(LogRecorder.Record record : logRecorder) { - logRecords.add(logRecordToObject(record)); + if (record.getId() > lastLogId) + { + logRecords.add(logRecordToObject(record)); + } } final PrintWriter writer = response.getWriter(); diff --git a/java/broker-plugins/management-http/src/main/java/resources/css/common.css b/java/broker-plugins/management-http/src/main/java/resources/css/common.css index 4c8b79ab82..e45c9cb463 100644 --- a/java/broker-plugins/management-http/src/main/java/resources/css/common.css +++ b/java/broker-plugins/management-http/src/main/java/resources/css/common.css @@ -92,4 +92,60 @@ div .messages { .formLabel-labelCell { font-weight: bold; -}
\ No newline at end of file +} + +.columnDefDialogButtonIcon { + background: url("../dojo/dojox/grid/enhanced/resources/images/sprite_icons.png") no-repeat; + background-position: -260px 2px; + width: 14px; + height: 14px; +} + +.logViewerIcon { + background: url("../images/log-viewer.png") no-repeat; + width: 14px; + height: 16px; +} + +.downloadLogsIcon { + background: url("../images/download.png") no-repeat; + width: 14px; + height: 14px; +} + +.dojoxGridFBarClearFilterButtontnIcon +{ + background: url("../dojo/dojox/grid/enhanced/resources/images/sprite_icons.png") no-repeat; + background-position: -120px -18px; + width: 14px; + height: 14px; +} + +.rowNumberLimitIcon +{ + background: url("../dojo/dojox/grid/enhanced/resources/images/sprite_icons.png") no-repeat; + background-position: -240px -18px; + width: 14px; + height: 14px; +} + +.gridRefreshIcon +{ + background: url("../images/refresh.png") no-repeat; + width: 16px; + height: 16px; +} + +.gridAutoRefreshIcon +{ + background: url("../images/auto-refresh.png") no-repeat; + width: 16px; + height: 16px; +} + +.redBackground tr{ background-color:#ffdcd7 !important; background-image: none !important;} +.yellowBackground tr{background-color:#fbfddf !important; background-image: none !important;} +.grayBackground tr{background-color:#eeeeee !important; background-image: none !important;} +.dojoxGridRowOdd.grayBackground tr{ background-color:#e9e9e9 !important; background-image: none !important;} +.dojoxGridRowOdd.yellowBackground tr{background-color:#fafdd5 !important; background-image: none !important;} +.dojoxGridRowOdd.redBackground tr{background-color:#f4c1c1 !important; background-image: none !important;}
\ No newline at end of file diff --git a/java/broker-plugins/management-http/src/main/java/resources/grid/showColumnDefDialog.html b/java/broker-plugins/management-http/src/main/java/resources/grid/showColumnDefDialog.html new file mode 100644 index 0000000000..5b6b8ad774 --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/grid/showColumnDefDialog.html @@ -0,0 +1,32 @@ +<!-- + - + - Licensed to the Apache Software Foundation (ASF) under one + - or more contributor license agreements. See the NOTICE file + - distributed with this work for additional information + - regarding copyright ownership. The ASF licenses this file + - to you under the Apache License, Version 2.0 (the + - "License"); you may not use this file except in compliance + - with the License. You may obtain a copy of the License at + - + - http://www.apache.org/licenses/LICENSE-2.0 + - + - Unless required by applicable law or agreed to in writing, + - software distributed under the License is distributed on an + - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + - KIND, either express or implied. See the License for the + - specific language governing permissions and limitations + - under the License. + - + --> +<div> + <div> + <div>Select columns to display:</div> + <div class="columnList"></div> + </div> + <div class="dijitDialogPaneActionBar"> + <button value="Display" data-dojo-type="dijit.form.Button" + class="displayButton" data-dojo-props="label: 'Display' "></button> + <button value="Cancel" data-dojo-type="dijit.form.Button" data-dojo-props="label: 'Cancel'" + class="cancelButton"></button> + </div> +</div> diff --git a/java/broker-plugins/management-http/src/main/java/resources/grid/showRowNumberLimitDialog.html b/java/broker-plugins/management-http/src/main/java/resources/grid/showRowNumberLimitDialog.html new file mode 100644 index 0000000000..087d54c0f9 --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/grid/showRowNumberLimitDialog.html @@ -0,0 +1,33 @@ +<!-- + - + - Licensed to the Apache Software Foundation (ASF) under one + - or more contributor license agreements. See the NOTICE file + - distributed with this work for additional information + - regarding copyright ownership. The ASF licenses this file + - to you under the Apache License, Version 2.0 (the + - "License"); you may not use this file except in compliance + - with the License. You may obtain a copy of the License at + - + - http://www.apache.org/licenses/LICENSE-2.0 + - + - Unless required by applicable law or agreed to in writing, + - software distributed under the License is distributed on an + - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + - KIND, either express or implied. See the License for the + - specific language governing permissions and limitations + - under the License. + - + --> +<div> + <div> + <div>Set the maximum number of rows to cache and display:</div> + <input class="rowNumberLimit" data-dojo-type="dijit.form.NumberSpinner" + data-dojo-props="invalidMessage: 'Invalid value', required: true, smallDelta: 1,mconstraints: {min:1,max:65535,places:0, pattern: '#####'}, label: 'Maximum number of rows:', name: 'rowNumberLimit'"></input> + </div> + <div class="dijitDialogPaneActionBar"> + <button value="Submit" data-dojo-type="dijit.form.Button" + class="submitButton" data-dojo-props="label: 'Submit' "></button> + <button value="Cancel" data-dojo-type="dijit.form.Button" data-dojo-props="label: 'Cancel'" + class="cancelButton"></button> + </div> +</div> diff --git a/java/broker-plugins/management-http/src/main/java/resources/images/auto-refresh.png b/java/broker-plugins/management-http/src/main/java/resources/images/auto-refresh.png Binary files differnew file mode 100644 index 0000000000..493636f467 --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/images/auto-refresh.png diff --git a/java/broker-plugins/management-http/src/main/java/resources/images/download.png b/java/broker-plugins/management-http/src/main/java/resources/images/download.png Binary files differnew file mode 100644 index 0000000000..b64b41d476 --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/images/download.png diff --git a/java/broker-plugins/management-http/src/main/java/resources/images/log-viewer.png b/java/broker-plugins/management-http/src/main/java/resources/images/log-viewer.png Binary files differnew file mode 100644 index 0000000000..858fd48beb --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/images/log-viewer.png diff --git a/java/broker-plugins/management-http/src/main/java/resources/images/refresh.png b/java/broker-plugins/management-http/src/main/java/resources/images/refresh.png Binary files differnew file mode 100644 index 0000000000..083044979b --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/images/refresh.png diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/UpdatableStore.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/UpdatableStore.js index f7ede1a7f7..ea3ba78372 100644 --- a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/UpdatableStore.js +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/UpdatableStore.js @@ -23,12 +23,13 @@ define(["dojo/store/Memory", "dojo/data/ObjectStore", "dojo/store/Observable"], function (Memory, DataGrid, ObjectStore, Observable) { - function UpdatableStore( data, divName, structure, func, props, Grid ) { + function UpdatableStore( data, divName, structure, func, props, Grid, notObservable ) { var that = this; var GridType = DataGrid; - that.store = Observable(Memory({data: data, idProperty: "id"})); + that.memoryStore = new Memory({data: data, idProperty: "id"}); + that.store = notObservable? that.memoryStore : new Observable(that.memoryStore); that.dataStore = ObjectStore({objectStore: that.store}); var gridProperties = { store: that.dataStore, @@ -63,7 +64,7 @@ define(["dojo/store/Memory", UpdatableStore.prototype.update = function(data) { - + var changed = false; var store = this.store; var theItem; @@ -78,7 +79,7 @@ define(["dojo/store/Memory", } } store.remove(object.id); - + changed = true; }); // iterate over data... @@ -91,20 +92,84 @@ define(["dojo/store/Memory", if(theItem[ propName ] != data[i][ propName ]) { theItem[ propName ] = data[i][ propName ]; modified = true; + changed = true; } } } if(modified) { // ... check attributes for updates store.notify(theItem, data[i].id); + changed = true; } } else { // ,,, if not in the store then add store.put(data[i]); + changed = true; } } } + return changed; + }; + + function removeItemsFromArray(items, numberToRemove) + { + if (items) + { + if (numberToRemove > 0 && items.length > 0) + { + if (numberToRemove >= items.length) + { + numberToRemove = numberToRemove - items.length; + items.length = 0 + } + else + { + items.splice(0, numberToRemove); + numberToRemove = 0; + } + } + } + return numberToRemove; + }; + + UpdatableStore.prototype.append = function(data, limit) + { + var changed = false; + var items = this.memoryStore.data; + + if (limit) + { + var totalSize = items.length + (data ? data.length : 0); + var numberToRemove = totalSize - limit; + + if (numberToRemove > 0) + { + changed = true; + numberToRemove = removeItemsFromArray(items, numberToRemove); + if (numberToRemove > 0) + { + removeItemsFromArray(data, numberToRemove); + } + } + } + + if (data && data.length > 0) + { + changed = true; + items.push.apply(items, data); + } + + this.memoryStore.setData(items); + return changed; + }; + + UpdatableStore.prototype.close = function() + { + this.dataStore.close(); + this.dataStore = null; + this.store = null; + this.memoryStore = null; }; return UpdatableStore; }); diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/ColumnDefDialog.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/ColumnDefDialog.js new file mode 100644 index 0000000000..d285dfaad6 --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/ColumnDefDialog.js @@ -0,0 +1,140 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +define([ + "dojo/_base/declare", + "dojo/_base/event", + "dojo/_base/array", + "dojo/_base/lang", + "dojo/parser", + "dojo/dom-construct", + "dojo/query", + "dijit/registry", + "dijit/form/Button", + "dijit/form/CheckBox", + "dojox/grid/enhanced/plugins/Dialog", + "dojo/text!../../../grid/showColumnDefDialog.html", + "dojo/domReady!" +], function(declare, event, array, lang, parser, dom, query, registry, Button, CheckBox, Dialog, template ){ + + +return declare("qpid.common.grid.ColumnDefDialog", null, { + + grid: null, + containerNode: null, + _columns: [], + _dialog: null, + + constructor: function(args){ + var grid = this.grid = args.grid; + + this.containerNode = dom.create("div", {innerHTML: template}); + parser.parse(this.containerNode); + + var submitButton = registry.byNode(query(".displayButton", this.containerNode)[0]); + this.closeButton = registry.byNode(query(".cancelButton", this.containerNode)[0]); + var columnsContainer = query(".columnList", this.containerNode)[0]; + + this._buildColumnWidgets(columnsContainer); + + this._dialog = new Dialog({ + "refNode": this.grid.domNode, + "title": "Grid Columns", + "content": this.containerNode + }); + + var self = this; + submitButton.on("click", function(e){self._onColumnsSelect(e); }); + this.closeButton.on("click", function(e){self._dialog.hide(); }); + + this._dialog.startup(); + }, + + destroy: function(){ + this._dialog.destroyRecursive(); + this._dialog = null; + this.grid = null; + this.containerNode = null; + this._columns = null; + }, + + showDialog: function(){ + this._initColumnWidgets(); + this._dialog.show(); + }, + + _initColumnWidgets: function() + { + var cells = this.grid.layout.cells; + for(var i in cells) + { + var cell = cells[i]; + this._columns[cell.name].checked = !cell.hidden; + } + }, + + _onColumnsSelect: function(evt){ + event.stop(evt); + var grid = this.grid; + grid.beginUpdate(); + var cells = grid.layout.cells; + try + { + for(var i in cells) + { + var cell = cells[i]; + var widget = this._columns[cell.name]; + grid.layout.setColumnVisibility(i, widget.checked); + } + } + finally + { + grid.endUpdate(); + this._dialog.hide(); + } + }, + + _buildColumnWidgets: function(columnsContainer) + { + var cells = this.grid.layout.cells; + for(var i in cells) + { + var cell = cells[i]; + var widget = new dijit.form.CheckBox({ + required: false, + checked: !cell.hidden, + label: cell.name, + name: this.grid.id + "_cchb_ " + i + }); + + this._columns[cell.name] = widget; + + var div = dom.create("div"); + div.appendChild(widget.domNode); + div.appendChild(dom.create("span", {innerHTML: cell.name})); + + columnsContainer.appendChild(div); + } + } + + }); + +}); diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/EnhancedFilter.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/EnhancedFilter.js new file mode 100644 index 0000000000..c882447f5d --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/EnhancedFilter.js @@ -0,0 +1,229 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +define([ + "dojo/_base/declare", + "dojo/_base/lang", + "dojo/_base/array", + "dijit/Toolbar", + "dojox/grid/enhanced/_Plugin", + "dojox/grid/enhanced/plugins/Dialog", + "dojox/grid/enhanced/plugins/filter/FilterLayer", + "dojox/grid/enhanced/plugins/filter/FilterDefDialog", + "dojox/grid/enhanced/plugins/filter/FilterStatusTip", + "dojox/grid/enhanced/plugins/filter/ClearFilterConfirm", + "dojox/grid/EnhancedGrid", + "dojo/i18n!dojox/grid/enhanced/nls/Filter", + "qpid/common/grid/EnhancedFilterTools" +], function(declare, lang, array, Toolbar, _Plugin, + Dialog, FilterLayer, FilterDefDialog, FilterStatusTip, ClearFilterConfirm, EnhancedGrid, nls, EnhancedFilterTools){ + + // override CriteriaBox#_getColumnOptions to show criteria for hidden columns with EnhancedFilter + dojo.extend(dojox.grid.enhanced.plugins.filter.CriteriaBox, { + _getColumnOptions: function(){ + var colIdx = this.dlg.curColIdx >= 0 ? String(this.dlg.curColIdx) : "anycolumn"; + var filterHidden = this.plugin.filterHidden; + return array.map(array.filter(this.plugin.grid.layout.cells, function(cell){ + return !(cell.filterable === false || (!filterHidden && cell.hidden)); + }), function(cell){ + return { + label: cell.name || cell.field, + value: String(cell.index), + selected: colIdx == String(cell.index) + }; + }); + } + }); + + // Enhanced filter has extra functionality for refreshing, limiting rows, displaying/hiding columns in the grid + var EnhancedFilter = declare("qpid.common.grid.EnhancedFilter", _Plugin, { + // summary: + // Accept the same plugin parameters as dojox.grid.enhanced.plugins.Filter and the following: + // + // filterHidden: boolean: + // Whether to display filtering criteria for hidden columns. Default to true. + // + // defaulGridRowLimit: int: + // Default limit for numbers of items to cache in the gris dtore + // + // disableFiltering: boolean: + // Whether to disable a filtering including filter button, clear filter button and filter summary. + // + // toolbar: dijit.Toolbar: + // An instance of toolbar to add the enhanced filter widgets. + + + // name: String + // plugin name + name: "enhancedFilter", + + // filterHidden: Boolean + // whether to filter hidden columns + filterHidden: true, + + constructor: function(grid, args){ + // summary: + // See constructor of dojox.grid.enhanced._Plugin. + this.grid = grid; + this.nls = nls; + + args = this.args = lang.isObject(args) ? args : {}; + if(typeof args.ruleCount != 'number' || args.ruleCount < 0){ + args.ruleCount = 0; + } + var rc = this.ruleCountToConfirmClearFilter = args.ruleCountToConfirmClearFilter; + if(rc === undefined){ + this.ruleCountToConfirmClearFilter = 5; + } + + if (args.filterHidden){ + this.filterHidden = args.filterHidden; + } + this.defaulGridRowLimit = args.defaulGridRowLimit; + this.disableFiltering = args.disableFiltering; + + //Install UI components + var obj = { "plugin": this }; + + this.filterBar = ( args.toolbar && args.toolbar instanceof dijit.Toolbar) ? args.toolbar: new Toolbar(); + + if (!this.disableFiltering) + { + //Install filter layer + this._wrapStore(); + + this.clearFilterDialog = new Dialog({ + refNode: this.grid.domNode, + title: this.nls["clearFilterDialogTitle"], + content: new ClearFilterConfirm(obj) + }); + + this.filterDefDialog = new FilterDefDialog(obj); + + nls["statusTipTitleNoFilter"] = "Filter is not set"; + nls["statusTipMsg"] = "Click on 'Set Filter' button to specify filtering conditions"; + this.filterStatusTip = new FilterStatusTip(obj); + + var self = this; + var toggleClearFilterBtn = function (arg){ self.enhancedFilterTools.toggleClearFilterBtn(arg); }; + + this.filterBar.toggleClearFilterBtn = toggleClearFilterBtn; + + this.grid.isFilterBarShown = function (){return true}; + + this.connect(this.grid.layer("filter"), "onFilterDefined", function(filter){ + toggleClearFilterBtn(true); + }); + + //Expose the layer event to grid. + grid.onFilterDefined = function(){}; + this.connect(grid.layer("filter"), "onFilterDefined", function(filter){ + grid.onFilterDefined(grid.getFilter(), grid.getFilterRelation()); + }); + } + + // add extra buttons into toolbar + this.enhancedFilterTools = new EnhancedFilterTools({ + grid: grid, + toolbar: this.filterBar, + filterStatusTip: this.filterStatusTip, + clearFilterDialog: this.clearFilterDialog, + filterDefDialog: this.filterDefDialog, + defaulGridRowLimit: this.defaulGridRowLimit, + disableFiltering: this.disableFiltering, + nls: nls + }); + + this.filterBar.placeAt(this.grid.viewsHeaderNode, "before"); + this.filterBar.startup(); + + }, + + destroy: function(){ + this.inherited(arguments); + try + { + if (this.grid) + { + this.grid.unwrap("filter"); + this.grid = null; + } + if (this.filterBar) + { + this.filterBar.destroyRecursive(); + this.filterBar = null; + } + if (this.enhancedFilterTools) + { + this.enhancedFilterTools.destroy(); + this.enhancedFilterTools = null; + } + if (this.clearFilterDialog) + { + this.clearFilterDialog.destroyRecursive(); + this.clearFilterDialog = null; + } + if (this.filterStatusTip) + { + this.filterStatusTip.destroy(); + this.filterStatusTip = null; + } + if (this.filterDefDialog) + { + this.filterDefDialog.destroy(); + this.filterDefDialog = null; + } + this.args = null; + + }catch(e){ + console.warn("Filter.destroy() error:",e); + } + }, + + _wrapStore: function(){ + var g = this.grid; + var args = this.args; + var filterLayer = args.isServerSide ? new FilterLayer.ServerSideFilterLayer(args) : + new FilterLayer.ClientSideFilterLayer({ + cacheSize: args.filterCacheSize, + fetchAll: args.fetchAllOnFirstFilter, + getter: this._clientFilterGetter + }); + FilterLayer.wrap(g, "_storeLayerFetch", filterLayer); + + this.connect(g, "_onDelete", lang.hitch(filterLayer, "invalidate")); + }, + + onSetStore: function(store){ + this.filterDefDialog.clearFilter(true); + }, + + _clientFilterGetter: function(/* data item */ datarow,/* cell */cell, /* int */rowIndex){ + return cell.get(rowIndex, datarow); + } + + }); + + EnhancedGrid.registerPlugin(EnhancedFilter); + + return EnhancedFilter; + +}); diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/EnhancedFilterTools.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/EnhancedFilterTools.js new file mode 100644 index 0000000000..b1645b4905 --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/EnhancedFilterTools.js @@ -0,0 +1,270 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +define([ + "dojo/_base/declare", + "dojo/_base/event", + "dijit/form/Button", + "dijit/form/ToggleButton", + "qpid/common/grid/RowNumberLimitDialog", + "qpid/common/grid/ColumnDefDialog", + "qpid/common/grid/FilterSummary" +], function(declare, event, Button, ToggleButton, RowNumberLimitDialog, ColumnDefDialog, FilterSummary){ + + var _stopEvent = function (evt){ + try{ + if(evt && evt.preventDefault){ + event.stop(evt); + } + }catch(e){} + }; + + return declare("qpid.common.grid.EnhancedFilterTools", null, { + + grid: null, + filterBar: null, + filterStatusTip: null, + clearFilterDialog: null, + filterDefDialog: null, + + columnDefDialog: null, + columnDefButton: null, + filterDefButton: null, + clearFilterButton: null, + filterSummary: null, + setRowNumberLimitButton: null, + setRowNumberLimitDialog: null, + refreshButton: null, + autoRefreshButton: null, + + constructor: function(params) + { + this.inherited(arguments); + + this.filterBar = params.toolbar; + this.grid = params.grid; + this.filterStatusTip= params.filterStatusTip; + this.clearFilterDialog = params.clearFilterDialog; + this.filterDefDialog = params.filterDefDialog; + + this._addRefreshButtons(); + this._addRowLimitButton(params.defaulGridRowLimit); + this._addColumnsButton(); + + if (!params.disableFiltering) + { + this._addFilteringTools(params.nls); + } + }, + + toggleClearFilterBtn: function(clearFlag) + { + var filterLayer = this.grid.layer("filter"); + var filterSet = filterLayer && filterLayer.filterDef && filterLayer.filterDef(); + this.clearFilterButton.set("disabled", !filterSet); + }, + + destroy: function() + { + this.inherited(arguments); + + if (this.columnDefDialog) + { + this.columnDefDialog.destroy(); + this.columnDefDialog = null; + } + if (this.columnDefButton) + { + this.columnDefButton.destroy(); + this.columnDefButton = null; + } + if (this.filterDefButton) + { + this.filterDefButton.destroy(); + this.filterDefButton = null; + } + if (this.clearFilterButton) + { + this.clearFilterButton.destroy(); + this.clearFilterButton = null; + } + if (this.filterSummary) + { + this.filterSummary.destroy(); + this.filterSummary = null; + } + if (this.setRowNumberLimitButton) + { + this.setRowNumberLimitButton.destroy(); + this.setRowNumberLimitButton = null; + } + if (this.setRowNumberLimitDialog) + { + this.setRowNumberLimitDialog.destroy(); + this.setRowNumberLimitDialog = null; + } + if (this.refreshButton) + { + this.refreshButton.destroy(); + this.refreshButton = null; + } + if (this.autoRefreshButton) + { + this.autoRefreshButton.destroy(); + this.autoRefreshButton = null; + } + + this.grid = null; + this.filterBar = null; + this.filterStatusTip = null; + this.clearFilterDialog = null; + this.filterDefDialog = null; + }, + + _addRefreshButtons: function() + { + var self = this; + this.refreshButton = new dijit.form.Button({ + label: "Refresh", + type: "button", + iconClass: "gridRefreshIcon", + title: "Manual Refresh" + }); + + this.autoRefreshButton = new dijit.form.ToggleButton({ + label: "Auto Refresh", + type: "button", + iconClass: "gridAutoRefreshIcon", + title: "Auto Refresh" + }); + + this.autoRefreshButton.on("change", function(value){ + self.grid.updater.updatable=value; + self.refreshButton.set("disabled", value); + }); + + this.refreshButton.on("click", function(value){ + self.grid.updater.performUpdate(); + }); + + this.filterBar.addChild(this.autoRefreshButton); + this.filterBar.addChild(this.refreshButton); + }, + + _addRowLimitButton: function(defaulGridRowLimit) + { + var self = this; + this.setRowNumberLimitButton = new dijit.form.Button({ + label: "Set Row Limit", + type: "button", + iconClass: "rowNumberLimitIcon", + title: "Set Row Number Limit" + }); + this.setRowNumberLimitButton.set("title", "Set Row Number Limit (Current: " + defaulGridRowLimit +")"); + + this.setRowNumberLimitDialog = new RowNumberLimitDialog(this.grid.domNode, function(newLimit){ + if (newLimit > 0 && self.grid.updater.appendLimit != newLimit ) + { + self.grid.updater.appendLimit = newLimit; + self.grid.updater.performRefresh([]); + self.setRowNumberLimitButton.set("title", "Set Row Number Limit (Current: " + newLimit +")"); + } + }); + + this.setRowNumberLimitButton.on("click", function(evt){ + self.setRowNumberLimitDialog.showDialog(self.grid.updater.appendLimit); + }); + + this.filterBar.addChild(this.setRowNumberLimitButton); + }, + + _addColumnsButton: function() + { + var self = this; + this.columnDefDialog = new ColumnDefDialog({grid: this.grid}); + + this.columnDefButton = new dijit.form.Button({ + label: "Display Columns", + type: "button", + iconClass: "columnDefDialogButtonIcon", + title: "Show/Hide Columns" + }); + + this.columnDefButton.on("click", function(e){ + _stopEvent(e); + self.columnDefDialog.showDialog(); + }); + + this.filterBar.addChild(this.columnDefButton); + }, + + _addFilteringTools: function(nls) + { + var self = this; + + this.filterDefButton = new dijit.form.Button({ + "class": "dojoxGridFBarBtn", + label: "Set Filter", + iconClass: "dojoxGridFBarDefFilterBtnIcon", + showLabel: "true", + title: "Define filter" + }); + + this.clearFilterButton = new dijit.form.Button({ + "class": "dojoxGridFBarBtn", + label: "Clear filter", + iconClass: "dojoxGridFBarClearFilterButtontnIcon", + showLabel: "true", + title: "Clear filter", + disabled: true + }); + + + this.filterDefButton.on("click", function(e){ + _stopEvent(e); + self.filterDefDialog.showDialog(); + }); + + this.clearFilterButton.on("click", function(e){ + _stopEvent(e); + if (self.ruleCountToConfirmClearFilter && self.filterDefDialog.getCriteria() >= self.ruleCountToConfirmClearFilter) + { + self.clearFilterDialog.show(); + } + else + { + self.grid.layer("filter").filterDef(null); + self.toggleClearFilterBtn(true) + } + }); + + this.filterSummary = new FilterSummary({grid: this.grid, filterStatusTip: this.filterStatusTip, nls: nls}); + + this.filterBar.addChild(this.filterDefButton); + this.filterBar.addChild(this.clearFilterButton); + + this.filterBar.addChild(new dijit.ToolbarSeparator()); + this.filterBar.addChild(this.filterSummary, "last"); + this.filterBar.getColumnIdx = function(coordX){return self.filterSummary._getColumnIdx(coordX);}; + + } + }); +});
\ No newline at end of file diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/FilterSummary.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/FilterSummary.js new file mode 100644 index 0000000000..2b1d960fa5 --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/FilterSummary.js @@ -0,0 +1,173 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +define([ + "dojo/_base/declare", + "dojo/_base/lang", + "dojo/_base/html", + "dojo/query", + "dojo/dom-construct", + "dojo/string", + "dojo/on", + "dijit/_WidgetBase" +], function(declare, lang, html, query, domConstruct, string, on, _WidgetBase){ + +return declare("qpid.common.grid.FilterSummary", [_WidgetBase], { + + domNode: null, + itemName: null, + filterStatusTip: null, + grid: null, + _handle_statusTooltip: null, + _timeout_statusTooltip: 300, + _nls: null, + + constructor: function(params) + { + this.inherited(arguments); + this.itemName = params.itemsName; + this.initialize(params.filterStatusTip, params.grid); + this._nls = params.nls; + }, + + buildRendering: function(){ + this.inherited(arguments); + var itemsName = this.itemName || this._nls["defaultItemsName"]; + var message = string.substitute(this._nls["filterBarMsgNoFilterTemplate"], [0, itemsName ]); + this.domNode = domConstruct.create("span", {innerHTML: message, "class": "dijit dijitReset dijitInline dijitButtonInline", role: "presentation" }); + }, + + postCreate: function(){ + this.inherited(arguments); + on(this.domNode, "mouseenter", lang.hitch(this, this._onMouseEnter)); + on(this.domNode, "mouseleave", lang.hitch(this, this._onMouseLeave)); + on(this.domNode, "mousemove", lang.hitch(this, this._onMouseMove)); + }, + + destroy: function() + { + this.inherited(arguments); + this.itemName = null; + this.filterStatusTip = null; + this.grid = null; + this._handle_statusTooltip = null; + this._filteredClass = null; + this._nls = null; + }, + + initialize: function(filterStatusTip, grid) + { + this.filterStatusTip = filterStatusTip; + this.grid = grid; + if (this.grid) + { + var filterLayer = grid.layer("filter"); + this.connect(filterLayer, "onFiltered", this.onFiltered); + } + }, + + onFiltered: function(filteredSize, originSize) + { + try + { + var itemsName = this.itemName || this._nls["defaultItemsName"], + msg = "", g = this.grid, + filterLayer = g.layer("filter"); + if(filterLayer.filterDef()){ + msg = string.substitute(this._nls["filterBarMsgHasFilterTemplate"], [filteredSize, originSize, itemsName]); + }else{ + msg = string.substitute(this._nls["filterBarMsgNoFilterTemplate"], [originSize, itemsName]); + } + this.domNode.innerHTML = msg; + } + catch(e) + { + // swallow and log exception + // otherwise grid rendering is screwed + console.error(e); + } + }, + + _getColumnIdx: function(coordX){ + var headers = query("[role='columnheader']", this.grid.viewsHeaderNode); + var idx = -1; + for(var i = headers.length - 1; i >= 0; --i){ + var coord = html.position(headers[i]); + if(coordX >= coord.x && coordX < coord.x + coord.w){ + idx = i; + break; + } + } + if(idx >= 0 && this.grid.layout.cells[idx].filterable !== false){ + return idx; + }else{ + return -1; + } + }, + + _setStatusTipTimeout: function(){ + this._clearStatusTipTimeout(); + this._handle_statusTooltip = setTimeout(lang.hitch(this,this._showStatusTooltip),this._timeout_statusTooltip); + }, + + _clearStatusTipTimeout: function(){ + if (this._handle_statusTooltip){ + clearTimeout(this._handle_statusTooltip); + } + this._handle_statusTooltip = null; + }, + + _showStatusTooltip: function(){ + this._handle_statusTooltip = null; + if(this.filterStatusTip){ + this.filterStatusTip.showDialog(this._tippos.x, this._tippos.y, this._getColumnIdx(this._tippos.x)); + } + }, + + _updateTipPosition: function(evt){ + this._tippos = { + x: evt.pageX, + y: evt.pageY + }; + }, + + _onMouseEnter: function(e){ + this._updateTipPosition(e); + if(this.filterStatusTip){ + this._setStatusTipTimeout(); + } + }, + + _onMouseMove: function(e){ + if(this.filterStatusTip){ + this._setStatusTipTimeout(); + if(this._handle_statusTooltip){ + this._updateTipPosition(e); + } + } + }, + + _onMouseLeave: function(e){ + this._clearStatusTipTimeout(); + }, + }); + +});
\ No newline at end of file diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/GridUpdater.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/GridUpdater.js new file mode 100644 index 0000000000..bb1e32bc31 --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/GridUpdater.js @@ -0,0 +1,244 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + + +define(["dojo/_base/xhr", + "dojo/parser", + "dojo/_base/array", + "dojo/_base/lang", + "qpid/common/properties", + "qpid/common/updater", + "qpid/common/UpdatableStore", + "qpid/common/util", + "dojox/grid/EnhancedGrid", + "qpid/common/grid/EnhancedFilter", + "dojox/grid/enhanced/plugins/NestedSorting", + "dojo/domReady!"], + function (xhr, parser, array, lang, properties, updater, UpdatableStore, util, EnhancedGrid, EnhancedFilter, NestedSorting) { + + /* + * Construct GridUpdater from the following arguments: + * serviceUrl - service URL to fetch data for the grid. Optional, if data is specified + * data - array containing data for the grid. Optional, if serviceUrl is specified + * node - dom node or dom node id to + * structure, + * funct, + * gridProperties, + * gridConstructor + */ + function GridUpdater(args) { + + var self = this; + + // GridUpdater fields + this.updatable = args.hasOwnProperty("updatable") ? args.updatable : true ; + this.serviceUrl = args.serviceUrl; + this.updatableStore = null; + this.grid = null; + this.onUpdate = args.onUpdate; + this._args = args; + this.appendData = args.append; + this.appendLimit = args.appendLimit; + + // default grid properties + var gridProperties = { + autoHeight: true, + updateDelay: 0, // no delay updates when receiving notifications from a datastore + plugins: { + pagination: { + defaultPageSize: 25, + pageSizes: [10, 25, 50, 100], + description: true, + sizeSwitch: true, + pageStepper: true, + gotoButton: true, + maxPageStep: 4, + position: "bottom" + }, + enhancedFilter: {} + } + }; + + var filterPluginFound = false; + + // merge args grid properties with default grid properties + if(args && args.gridProperties) + { + var argProperties = args.gridProperties; + for(var argProperty in argProperties) + { + if(argProperties.hasOwnProperty(argProperty)) + { + if (argProperty == "plugins") + { + var argPlugins = argProperties[ argProperty ]; + for(var argPlugin in argPlugins) + { + if (argPlugin == "filter") + { + // we need to switch off filtering in EnhancedFilter + filterPluginFound = true; + } + if(argPlugins.hasOwnProperty(argPlugin)) + { + var argPluginProperties = argPlugins[ argPlugin ]; + if (argPluginProperties && gridProperties.plugins.hasOwnProperty(argPlugin)) + { + var gridPlugin = gridProperties.plugins[ argPlugin ]; + for(var pluginProperty in argPluginProperties) + { + if(argPluginProperties.hasOwnProperty(pluginProperty)) + { + gridPlugin[pluginProperty] = argPluginProperties[pluginProperty]; + } + } + } + else + { + gridProperties.plugins[ argPlugin ] = argPlugins[ argPlugin ]; + } + } + } + } + else + { + gridProperties[ argProperty ] = argProperties[ argProperty ]; + } + } + } + } + + if (filterPluginFound) + { + gridProperties.plugins.enhancedFilter.disableFiltering = true; + } + + var updatableStoreFactory = function(data) + { + try + { + self.updatableStore = new UpdatableStore(data, self._args.node, self._args.structure, + self._args.funct, gridProperties, self._args.GridConstructor || EnhancedGrid, self.appendData); + } + catch(e) + { + console.error(e); + throw e; + } + self.grid = self.updatableStore.grid; + self.grid.updater = self; + if (self.onUpdate) + { + self.onUpdate(data); + } + if (self.serviceUrl) + { + updater.add(self); + } + }; + + if (args && args.serviceUrl) + { + var requestUrl = lang.isFunction(this.serviceUrl) ? this.serviceUrl() : this.serviceUrl; + xhr.get({url: requestUrl, sync: properties.useSyncGet, handleAs: "json"}).then(updatableStoreFactory, util.errorHandler); + } + else if (args && args.data) + { + updatableStoreFactory(args.data); + } + } + + GridUpdater.prototype.destroy = function() + { + updater.remove(this); + if (this.updatableStore) + { + this.updatableStore.close(); + this.updatableStore = null; + } + if (this.grid) + { + this.grid.destroy(); + this.grid = null; + } + }; + + GridUpdater.prototype.refresh = function(data) + { + this.updating = true; + try + { + if (this.appendData ? this.updatableStore.append(data, this.appendLimit): this.updatableStore.update(data)) + { + // EnhancedGrid with Filter plugin has "filter" layer. + // The filter expression needs to be re-applied after the data update + var filterLayer = this.grid.layer("filter"); + if ( filterLayer && filterLayer.filterDef) + { + var currentFilter = filterLayer.filterDef(); + + if (currentFilter) + { + // re-apply filter in the filter layer + filterLayer.filterDef(currentFilter); + } + } + + // refresh grid to render updates + this.grid._refresh(); + } + } + finally + { + this.updating = false; + if (this.onUpdate) + { + this.onUpdate(data); + } + } + } + + GridUpdater.prototype.update = function() + { + if (this.updatable) + { + this.performUpdate(); + } + }; + + GridUpdater.prototype.performUpdate = function() + { + var self = this; + var requestUrl = lang.isFunction(this.serviceUrl) ? this.serviceUrl() : this.serviceUrl; + var requestArguments = {url: requestUrl, sync: properties.useSyncGet, handleAs: "json"}; + xhr.get(requestArguments).then(function(data){self.refresh(data);}); + }; + + GridUpdater.prototype.performRefresh = function(data) + { + if (!this.updating) + { + this.refresh(data); + } + }; + + return GridUpdater; + }); diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/RowNumberLimitDialog.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/RowNumberLimitDialog.js new file mode 100644 index 0000000000..db3ae5a2ea --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/grid/RowNumberLimitDialog.js @@ -0,0 +1,96 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +define([ + "dojo/_base/declare", + "dojo/_base/event", + "dojo/_base/array", + "dojo/_base/lang", + "dojo/parser", + "dojo/dom-construct", + "dojo/query", + "dijit/registry", + "dijit/form/Button", + "dijit/form/CheckBox", + "dojox/grid/enhanced/plugins/Dialog", + "dojo/text!../../../grid/showRowNumberLimitDialog.html", + "dojo/domReady!" +], function(declare, event, array, lang, parser, dom, query, registry, Button, CheckBox, Dialog, template ){ + + +return declare("qpid.management.logs.RowNumberLimitDialog", null, { + + grid: null, + dialog: null, + + constructor: function(domNode, limitChangedCallback){ + + this.containerNode = dom.create("div", {innerHTML: template}); + parser.parse(this.containerNode); + + this.rowNumberLimit = registry.byNode(query(".rowNumberLimit", this.containerNode)[0]) + this.submitButton = registry.byNode(query(".submitButton", this.containerNode)[0]); + this.closeButton = registry.byNode(query(".cancelButton", this.containerNode)[0]); + + this.dialog = new Dialog({ + "refNode": domNode, + "title": "Grid Rows Number", + "content": this.containerNode + }); + + var self = this; + this.submitButton.on("click", function(e){ + if (self.rowNumberLimit.value > 0) + { + try + { + limitChangedCallback(self.rowNumberLimit.value); + } + catch(e) + { + console.error(e); + } + finally + { + self.dialog.hide(); + } + } + }); + + this.closeButton.on("click", function(e){self.dialog.hide(); }); + this.dialog.startup(); + }, + + destroy: function(){ + this.submitButton.destroy(); + this.closeButton.destroy(); + this.dialog.destroy(); + this.dialog = null; + }, + + showDialog: function(currentLimit){ + this.rowNumberLimit.set("value", currentLimit); + this.dialog.show(); + } + + }); + +}); diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js index 2c2096d390..86fda92cb5 100644 --- a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js @@ -353,5 +353,21 @@ define(["dojo/_base/xhr", } }; + util.errorHandler = function errorHandler(error) + { + if(error.status == 401) + { + alert("Authentication Failed"); + } + else if(error.status == 403) + { + alert("Access Denied"); + } + else + { + alert(error); + } + } + return util; });
\ No newline at end of file diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Broker.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Broker.js index f721ad6fa5..1bb0ca0afa 100644 --- a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Broker.js +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Broker.js @@ -352,6 +352,12 @@ define(["dojo/_base/xhr", that.brokerUpdater.update(); + var logViewerButton = query(".logViewer", contentPane.containerNode)[0]; + registry.byNode(logViewerButton).on("click", function(evt){ + that.controller.show("logViewer", null, null); + }); + + var addProviderButton = query(".addAuthenticationProvider", contentPane.containerNode)[0]; connect.connect(registry.byNode(addProviderButton), "onClick", function(evt){ addAuthenticationProvider.show(); }); @@ -664,43 +670,6 @@ define(["dojo/_base/xhr", }, gridProperties, EnhancedGrid); that.displayACLWarnMessage(aclData); }); - - xhr.get({url: "rest/logrecords", sync: properties.useSyncGet, handleAs: "json"}) - .then(function(data) - { - that.logData = data; - - var gridProperties = { - height: 400, - plugins: { - pagination: { - pageSizes: ["10", "25", "50", "100"], - description: true, - sizeSwitch: true, - pageStepper: true, - gotoButton: true, - maxPageStep: 4, - position: "bottom" - } - }}; - - - that.logfileGrid = - new UpdatableStore(that.logData, query(".broker-logfile")[0], - [ { name: "Timestamp", field: "timestamp", width: "200px", - formatter: function(val) { - var d = new Date(0); - d.setUTCSeconds(val/1000); - - return d.toLocaleString(); - }}, - { name: "Level", field: "level", width: "60px"}, - { name: "Logger", field: "logger", width: "280px"}, - { name: "Thread", field: "thread", width: "120px"}, - { name: "Log Message", field: "message", width: "100%"} - - ], null, gridProperties, EnhancedGrid); - }); } BrokerUpdater.prototype.updateHeader = function() @@ -805,15 +774,6 @@ define(["dojo/_base/xhr", that.displayACLWarnMessage(data); } }); - - - xhr.get({url: "rest/logrecords", sync: properties.useSyncGet, handleAs: "json"}) - .then(function(data) - { - that.logData = data; - that.logfileGrid.update(that.logData); - }); - }; BrokerUpdater.prototype.showReadOnlyAttributes = function() diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/controller.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/controller.js index b7eddbbb77..b487ab62f2 100644 --- a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/controller.js +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/controller.js @@ -35,10 +35,11 @@ define(["dojo/dom", "qpid/management/AccessControlProvider", "qpid/management/Port", "qpid/management/Plugin", + "qpid/management/logs/LogViewer", "dojo/ready", "dojo/domReady!"], function (dom, registry, ContentPane, entities, Broker, VirtualHost, Exchange, Queue, Connection, AuthProvider, - GroupProvider, Group, KeyStore, TrustStore, AccessControlProvider, Port, Plugin, ready) { + GroupProvider, Group, KeyStore, TrustStore, AccessControlProvider, Port, Plugin, LogViewer, ready) { var controller = {}; var constructors = { broker: Broker, virtualhost: VirtualHost, exchange: Exchange, @@ -46,7 +47,7 @@ define(["dojo/dom", authenticationprovider: AuthProvider, groupprovider: GroupProvider, group: Group, keystore: KeyStore, truststore: TrustStore, accesscontrolprovider: AccessControlProvider, port: Port, - plugin: Plugin}; + plugin: Plugin, logViewer: LogViewer}; var tabDiv = dom.byId("managedViews"); diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/logs/LogFileDownloadDialog.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/logs/LogFileDownloadDialog.js new file mode 100644 index 0000000000..c25fd7c609 --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/logs/LogFileDownloadDialog.js @@ -0,0 +1,175 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +define([ + "dojo/_base/declare", + "dojo/_base/event", + "dojo/_base/xhr", + "dojo/_base/connect", + "dojo/dom-construct", + "dojo/query", + "dojo/parser", + "dojo/store/Memory", + "dojo/data/ObjectStore", + "dojo/date/locale", + "dojo/number", + "dijit/registry", + "dijit/Dialog", + "dijit/form/Button", + "dojox/grid/EnhancedGrid", + "dojo/text!../../../logs/showLogFileDownloadDialog.html", + "dojo/domReady!" +], function(declare, event, xhr, connect, domConstruct, query, parser, Memory, ObjectStore, locale, number, + registry, Dialog, Button, EnhancedGrid, template){ + + +return declare("qpid.management.logs.LogFileDownloadDialog", null, { + + templateString: template, + containerNode: null, + widgetsInTemplate: true, + logFileDialog: null, + logFilesGrid: null, + downloadLogsButton: null, + closeButton: null, + + constructor: function(args){ + this.containerNode = domConstruct.create("div", {innerHTML: template}); + parser.parse(this.containerNode); + + this.logFileTreeDiv = query(".logFilesGrid", this.containerNode)[0]; + this.downloadLogsButton = registry.byNode(query(".downloadLogsButton", this.containerNode)[0]); + this.closeButton = registry.byNode(query(".downloadLogsDialogCloseButton", this.containerNode)[0]); + + var self = this; + this.closeButton.on("click", function(e){self._onCloseButtonClick(e);}); + this.downloadLogsButton.on("click", function(e){self._onDownloadButtonClick(e);}); + this.downloadLogsButton.set("disabled", true) + + this.logFileDialog = new Dialog({ + title:"Broker Log Files", + style: "width: 600px", + content: this.containerNode + }); + + var layout = [ + { name: "Appender", field: "appenderName", width: "auto"}, + { name: "Name", field: "name", width: "auto"}, + { name: "Size", field: "size", width: "60px", + formatter: function(val){ + return val > 1024 ? (val > 1048576? number.round(val/1048576) + "MB": number.round(val/1024) + "KB") : val + "bytes"; + } + }, + { name: "Last Modified", field: "lastModified", width: "250px", + formatter: function(val) { + var d = new Date(val); + return locale.format(d, {selector:"date", datePattern: "EEE, MMM d yy, HH:mm:ss z (ZZZZ)"}); + } + } + ]; + + var gridProperties = { + store: new ObjectStore({objectStore: new Memory({data: [], idProperty: "id"}) }), + structure: layout, + autoHeight: true, + plugins: { + pagination: { + pageSizes: [10, 25, 50, 100], + description: true, + sizeSwitch: true, + pageStepper: true, + gotoButton: true, + maxPageStep: 4, + position: "bottom" + }, + indirectSelection: { + headerSelector:true, + width:"20px", + styles:"text-align: center;" + } + } + }; + + this.logFilesGrid = new EnhancedGrid(gridProperties, this.logFileTreeDiv); + var self = this; + var downloadButtonToggler = function(rowIndex){ + var data = self.logFilesGrid.selection.getSelected(); + self.downloadLogsButton.set("disabled",!data.length ); + }; + connect.connect(this.logFilesGrid.selection, 'onSelected', downloadButtonToggler); + connect.connect(this.logFilesGrid.selection, 'onDeselected', downloadButtonToggler); + }, + + _onCloseButtonClick: function(evt){ + event.stop(evt); + this.logFileDialog.hide(); + }, + + _onDownloadButtonClick: function(evt){ + event.stop(evt); + var data = this.logFilesGrid.selection.getSelected(); + if (data.length) + { + var query = ""; + for(var i = 0 ; i< data.length; i++) + { + if (i>0) + { + query+="&"; + } + query+="l="+encodeURIComponent(data[i].appenderName +'/' + data[i].name); + } + window.location="rest/logfile?" + query; + this.logFileDialog.hide(); + } + }, + + destroy: function(){ + this.inherited(arguments); + if (this.logFileDialog) + { + this.logFileDialog.destroyRecursive(); + this.logFileDialog = null; + } + }, + + showDialog: function(){ + var self = this; + var requestArguments = {url: "rest/logfiles", sync: true, handleAs: "json"}; + xhr.get(requestArguments).then(function(data){ + try + { + self.logFilesGrid.store.objectStore.setData(data); + self.logFilesGrid.startup(); + self.logFileDialog.startup(); + self.logFileDialog.show(); + self.logFilesGrid._refresh(); + + } + catch(e) + { + console.error(e); + } + }); + } + + }); + +}); diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/logs/LogViewer.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/logs/LogViewer.js new file mode 100644 index 0000000000..4bec7440ab --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/logs/LogViewer.js @@ -0,0 +1,202 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +define(["dojo/_base/xhr", + "dojo/parser", + "dojo/query", + "dojo/date/locale", + "dijit/registry", + "qpid/common/grid/GridUpdater", + "qpid/management/logs/LogFileDownloadDialog", + "dojo/text!../../../logs/showLogViewer.html", + "dojo/domReady!"], + function (xhr, parser, query, locale, registry, GridUpdater, LogFileDownloadDialog, markup) { + + var defaulGridRowLimit = 4096; + + function LogViewer(name, parent, controller) { + var self = this; + + this.name = name; + this.lastLogId = 0; + this.contentPane = null; + this.downloadLogsButton = null; + this.downloadLogDialog = null; + } + + LogViewer.prototype.getTitle = function() { + return "Log Viewer"; + }; + + LogViewer.prototype.open = function(contentPane) { + var self = this; + this.contentPane = contentPane; + this.contentPane.containerNode.innerHTML = markup; + + parser.parse(this.contentPane.containerNode); + + this.downloadLogsButton = registry.byNode(query(".downloadLogs", contentPane.containerNode)[0]); + this.downloadLogDialog = new LogFileDownloadDialog(); + + this.downloadLogsButton.on("click", function(evt){ + self.downloadLogDialog.showDialog(); + }); + + var gridStructure = [ + { + hidden: true, + name: "ID", + field: "id", + width: "50px", + datatype: "number", + filterable: true + }, + { + name: "Date", field: "timestamp", width: "100px", datatype: "date", + formatter: function(val) { + var d = new Date(0); + d.setUTCSeconds(val/1000); + return locale.format(d, {selector:"date", datePattern: "EEE, MMM d yy"}); + }, + dataTypeArgs: { + selector: "date", + datePattern: "EEE MMMM d yyy" + } + }, + { name: "Time", field: "timestamp", width: "150px", datatype: "time", + formatter: function(val) { + var d = new Date(0); + d.setUTCSeconds(val/1000); + return locale.format(d, {selector:"time", timePattern: "HH:mm:ss z (ZZZZ)"}); + }, + dataTypeArgs: { + selector: "time", + timePattern: "HH:mm:ss ZZZZ" + } + }, + { name: "Level", field: "level", width: "50px", datatype: "string", autoComplete: true, hidden: true}, + { name: "Logger", field: "logger", width: "150px", datatype: "string", autoComplete: false, hidden: true}, + { name: "Thread", field: "thread", width: "100px", datatype: "string", hidden: true}, + { name: "Log Message", field: "message", width: "auto", datatype: "string"} + ]; + + this._buildGrid(gridStructure); + }; + + LogViewer.prototype._buildGrid = function(gridStructure) { + var self = this; + var gridNode = query("#broker-logfile", this.contentPane.containerNode)[0]; + try + { + this.updater = new GridUpdater({ + updatable: false, + serviceUrl: function() + { + return "rest/logrecords?lastLogId=" + self.lastLogId; + }, + onUpdate: function(items) + { + if (items) + { + var maxId = -1; + for(var i in items) + { + var item = items[i]; + if (item.id > maxId) + { + maxId = item.id + } + } + if (maxId != -1) + { + self.lastLogId = maxId + } + } + }, + append: true, + appendLimit: defaulGridRowLimit, + node: gridNode, + structure: gridStructure, + gridProperties: { + selectable: true, + selectionMode: "none", + sortInfo: -1, + sortFields: [{attribute: 'timestamp', descending: true}], + plugins:{ + nestedSorting:true, + enhancedFilter:{defaulGridRowLimit: defaulGridRowLimit}, + indirectSelection: false + } + }, + funct: function (obj) + { + var onStyleRow = function(row) + { + var item = obj.grid.getItem(row.index); + if(item){ + var level = obj.dataStore.getValue(item, "level", null); + var changed = false; + if(level == "ERROR"){ + row.customClasses += " redBackground"; + changed = true; + } else if(level == "WARN"){ + row.customClasses += " yellowBackground"; + changed = true; + } else if(level == "DEBUG"){ + row.customClasses += " grayBackground"; + changed = true; + } + if (changed) + { + obj.grid.focus.styleRow(row); + } + } + }; + obj.grid.on("styleRow", onStyleRow); + obj.grid.startup(); + } + }); + } + catch(err) + { + console.error(err); + } + }; + + LogViewer.prototype.close = function() { + if (this.updater) + { + this.updater.destroy(); + this.updater = null; + } + if (this.downloadLogDialog) + { + this.downloadLogDialog.destroy(); + this.downloadLogDialog = null; + } + if (this.downloadLogsButton) + { + this.downloadLogsButton.destroy(); + this.downloadLogsButton = null; + } + }; + + return LogViewer; + }); diff --git a/java/broker-plugins/management-http/src/main/java/resources/logs/showLogFileDownloadDialog.html b/java/broker-plugins/management-http/src/main/java/resources/logs/showLogFileDownloadDialog.html new file mode 100644 index 0000000000..bc633d059a --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/logs/showLogFileDownloadDialog.html @@ -0,0 +1,33 @@ +<!-- + - + - Licensed to the Apache Software Foundation (ASF) under one + - or more contributor license agreements. See the NOTICE file + - distributed with this work for additional information + - regarding copyright ownership. The ASF licenses this file + - to you under the Apache License, Version 2.0 (the + - "License"); you may not use this file except in compliance + - with the License. You may obtain a copy of the License at + - + - http://www.apache.org/licenses/LICENSE-2.0 + - + - Unless required by applicable law or agreed to in writing, + - software distributed under the License is distributed on an + - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + - KIND, either express or implied. See the License for the + - specific language governing permissions and limitations + - under the License. + - + --> +<div> + <div class="contentArea" style="height:320px;overflow:auto"> + <div><b>Select log files to download</b></div> + <div class="logFilesGrid" style='height:300px;width: 580px'></div> + </div> + <div class="dijitDialogPaneActionBar"> + <button value="Download" data-dojo-type="dijit.form.Button" + class="downloadLogsButton" + data-dojo-props="iconClass: 'downloadLogsIcon', label: 'Download' "></button> + <button value="Close" data-dojo-type="dijit.form.Button" data-dojo-props="label: 'Close'" + class="downloadLogsDialogCloseButton"></button> + </div> +</div> diff --git a/java/broker-plugins/management-http/src/main/java/resources/logs/showLogViewer.html b/java/broker-plugins/management-http/src/main/java/resources/logs/showLogViewer.html new file mode 100644 index 0000000000..10ac09a406 --- /dev/null +++ b/java/broker-plugins/management-http/src/main/java/resources/logs/showLogViewer.html @@ -0,0 +1,29 @@ +<!-- + - + - Licensed to the Apache Software Foundation (ASF) under one + - or more contributor license agreements. See the NOTICE file + - distributed with this work for additional information + - regarding copyright ownership. The ASF licenses this file + - to you under the Apache License, Version 2.0 (the + - "License"); you may not use this file except in compliance + - with the License. You may obtain a copy of the License at + - + - http://www.apache.org/licenses/LICENSE-2.0 + - + - Unless required by applicable law or agreed to in writing, + - software distributed under the License is distributed on an + - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + - KIND, either express or implied. See the License for the + - specific language governing permissions and limitations + - under the License. + - + --> +<div class="logViewer"> + + <div id="broker-logfile"></div> + <br/> + <button data-dojo-type="dijit.form.Button" class="downloadLogs" + data-dojo-props="iconClass: 'downloadLogsIcon', title:'Download Log Files', name: 'downloadLogs'">Download</button> + <br/> +</div> + diff --git a/java/broker-plugins/management-http/src/main/java/resources/showBroker.html b/java/broker-plugins/management-http/src/main/java/resources/showBroker.html index d9991452af..366ef27c8a 100644 --- a/java/broker-plugins/management-http/src/main/java/resources/showBroker.html +++ b/java/broker-plugins/management-http/src/main/java/resources/showBroker.html @@ -190,9 +190,7 @@ <button data-dojo-type="dijit.form.Button" class="deleteAccessControlProvider">Delete Access Control Provider</button> </div> <br/> - <div data-dojo-type="dijit.TitlePane" data-dojo-props="title: 'Log File', open: false"> - <div class="broker-logfile"></div> - </div> - <br/> + <button data-dojo-type="dijit.form.Button" class="logViewer" data-dojo-props="iconClass: 'logViewerIcon'">Log Viewer</button> + <br/><br/> </div> diff --git a/java/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/log/LogFileHelperTest.java b/java/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/log/LogFileHelperTest.java new file mode 100644 index 0000000000..608ef28f02 --- /dev/null +++ b/java/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/log/LogFileHelperTest.java @@ -0,0 +1,339 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.server.management.plugin.log; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.apache.log4j.Appender; +import org.apache.log4j.DailyRollingFileAppender; +import org.apache.log4j.FileAppender; +import org.apache.log4j.QpidCompositeRollingAppender; +import org.apache.log4j.RollingFileAppender; +import org.apache.log4j.varia.ExternallyRolledFileAppender; +import org.apache.qpid.test.utils.QpidTestCase; +import org.apache.qpid.test.utils.TestFileUtils; +import org.apache.qpid.util.FileUtils; + +public class LogFileHelperTest extends QpidTestCase +{ + private Map<String, List<File>> _appendersFiles; + private File _compositeRollingAppenderBackupFolder; + private List<Appender> _appenders; + private LogFileHelper _helper; + + public void setUp() throws Exception + { + super.setUp(); + _appendersFiles = new HashMap<String, List<File>>(); + _compositeRollingAppenderBackupFolder = new File(TMP_FOLDER, "_compositeRollingAppenderBackupFolder"); + _compositeRollingAppenderBackupFolder.mkdirs(); + + _appendersFiles.put(FileAppender.class.getSimpleName(), + Collections.singletonList(TestFileUtils.createTempFile(this, ".log", "FileAppender"))); + _appendersFiles.put(DailyRollingFileAppender.class.getSimpleName(), + Collections.singletonList(TestFileUtils.createTempFile(this, ".log", "DailyRollingFileAppender"))); + _appendersFiles.put(RollingFileAppender.class.getSimpleName(), + Collections.singletonList(TestFileUtils.createTempFile(this, ".log", "RollingFileAppender"))); + _appendersFiles.put(ExternallyRolledFileAppender.class.getSimpleName(), + Collections.singletonList(TestFileUtils.createTempFile(this, ".log", "ExternallyRolledFileAppender"))); + + File file = TestFileUtils.createTempFile(this, ".log", "QpidCompositeRollingAppender"); + File backUpFile = File.createTempFile(file.getName() + ".", ".1." + LogFileHelper.GZIP_EXTENSION); + _appendersFiles.put(QpidCompositeRollingAppender.class.getSimpleName(), Arrays.asList(file, backUpFile)); + + FileAppender fileAppender = new FileAppender(); + DailyRollingFileAppender dailyRollingFileAppender = new DailyRollingFileAppender(); + RollingFileAppender rollingFileAppender = new RollingFileAppender(); + ExternallyRolledFileAppender externallyRolledFileAppender = new ExternallyRolledFileAppender(); + QpidCompositeRollingAppender qpidCompositeRollingAppender = new QpidCompositeRollingAppender(); + qpidCompositeRollingAppender.setbackupFilesToPath(_compositeRollingAppenderBackupFolder.getPath()); + + _appenders = new ArrayList<Appender>(); + _appenders.add(fileAppender); + _appenders.add(dailyRollingFileAppender); + _appenders.add(rollingFileAppender); + _appenders.add(externallyRolledFileAppender); + _appenders.add(qpidCompositeRollingAppender); + + for (Appender appender : _appenders) + { + FileAppender fa = (FileAppender) appender; + fa.setName(fa.getClass().getSimpleName()); + fa.setFile(_appendersFiles.get(fa.getClass().getSimpleName()).get(0).getPath()); + } + + _helper = new LogFileHelper(_appenders); + } + + public void tearDown() throws Exception + { + try + { + for (List<File> files : _appendersFiles.values()) + { + for (File file : files) + { + try + { + FileUtils.delete(file, false); + } + catch (Exception e) + { + // ignore + } + } + } + FileUtils.delete(_compositeRollingAppenderBackupFolder, true); + } + finally + { + super.tearDown(); + } + } + + public void testGetLogFileDetailsWithLocations() throws Exception + { + List<LogFileDetails> details = _helper.getLogFileDetails(true); + + assertLogFiles(details, true); + } + + public void testGetLogFileDetailsWithoutLocations() throws Exception + { + List<LogFileDetails> details = _helper.getLogFileDetails(false); + + assertLogFiles(details, false); + } + + public void testWriteLogFilesForAllLogs() throws Exception + { + List<LogFileDetails> details = _helper.getLogFileDetails(true); + File f = TestFileUtils.createTempFile(this, ".zip"); + + FileOutputStream os = new FileOutputStream(f); + try + { + _helper.writeLogFiles(details, os); + } + finally + { + if (os != null) + { + os.close(); + } + } + + assertWrittenFile(f, details); + } + + public void testWriteLogFile() throws Exception + { + File file = _appendersFiles.get(FileAppender.class.getSimpleName()).get(0); + + File f = TestFileUtils.createTempFile(this, ".log"); + FileOutputStream os = new FileOutputStream(f); + try + { + _helper.writeLogFile(file, os); + } + finally + { + if (os != null) + { + os.close(); + } + } + + assertEquals("Unexpected log content", FileAppender.class.getSimpleName(), FileUtils.readFileAsString(f)); + } + + public void testFindLogFileDetails() + { + String[] logFileDisplayedPaths = new String[6]; + File[] files = new File[logFileDisplayedPaths.length]; + int i = 0; + for (Map.Entry<String, List<File>> entry : _appendersFiles.entrySet()) + { + String appenderName = entry.getKey(); + List<File> appenderFiles = entry.getValue(); + for (File logFile : appenderFiles) + { + logFileDisplayedPaths[i] = appenderName + "/" + logFile.getName(); + files[i++] = logFile; + } + } + + List<LogFileDetails> logFileDetails = _helper.findLogFileDetails(logFileDisplayedPaths); + assertEquals("Unexpected details size", logFileDisplayedPaths.length, logFileDetails.size()); + + boolean gzipFileFound = false; + for (int j = 0; j < logFileDisplayedPaths.length; j++) + { + String displayedPath = logFileDisplayedPaths[j]; + String[] parts = displayedPath.split("/"); + LogFileDetails d = logFileDetails.get(j); + assertEquals("Unexpected name", parts[1], d.getName()); + assertEquals("Unexpected appender", parts[0], d.getAppenderName()); + if (files[j].getName().endsWith(LogFileHelper.GZIP_EXTENSION)) + { + assertEquals("Unexpected mime type for gz file", LogFileHelper.GZIP_MIME_TYPE, d.getMimeType()); + gzipFileFound = true; + } + else + { + assertEquals("Unexpected mime type", LogFileHelper.TEXT_MIME_TYPE, d.getMimeType()); + } + assertEquals("Unexpecte file location", files[j], d.getLocation()); + assertEquals("Unexpecte file size", files[j].length(), d.getSize()); + assertEquals("Unexpecte file last modified date", files[j].lastModified(), d.getLastModified()); + } + assertTrue("Gzip log file is not found", gzipFileFound); + } + + public void testFindLogFileDetailsForNotExistingAppender() + { + String[] logFileDisplayedPaths = { "NotExistingAppender/qpid.log" }; + List<LogFileDetails> details = _helper.findLogFileDetails(logFileDisplayedPaths); + assertTrue("No details should be created for non-existing appender", details.isEmpty()); + } + + public void testFindLogFileDetailsForNotExistingFile() + { + String[] logFileDisplayedPaths = { "FileAppender/qpid-non-existing.log" }; + List<LogFileDetails> details = _helper.findLogFileDetails(logFileDisplayedPaths); + assertTrue("No details should be created for non-existing file", details.isEmpty()); + } + + public void testFindLogFileDetailsForIncorectlySpecifiedLogFilePath() + { + String[] logFileDisplayedPaths = { "FileAppender\\" + _appendersFiles.get("FileAppender").get(0).getName() }; + try + { + _helper.findLogFileDetails(logFileDisplayedPaths); + fail("Exception is expected for incorectly set path to log file"); + } + catch (IllegalArgumentException e) + { + // pass + } + } + + private void assertLogFiles(List<LogFileDetails> details, boolean includeLocation) + { + for (Map.Entry<String, List<File>> appenderData : _appendersFiles.entrySet()) + { + String appenderName = (String) appenderData.getKey(); + List<File> files = appenderData.getValue(); + + for (File logFile : files) + { + String logFileName = logFile.getName(); + LogFileDetails d = findLogFileDetails(logFileName, appenderName, details); + assertNotNull("Log file " + logFileName + " is not found for appender " + appenderName, d); + if (includeLocation) + { + assertEquals("Log file " + logFileName + " is different in appender " + appenderName, d.getLocation(), + logFile); + } + } + } + } + + private LogFileDetails findLogFileDetails(String logFileName, String appenderName, List<LogFileDetails> logFileDetails) + { + LogFileDetails d = null; + for (LogFileDetails lfd : logFileDetails) + { + if (lfd.getName().equals(logFileName) && lfd.getAppenderName().equals(appenderName)) + { + d = lfd; + break; + } + } + return d; + } + + private void assertWrittenFile(File f, List<LogFileDetails> details) throws FileNotFoundException, IOException + { + FileInputStream fis = new FileInputStream(f); + try + { + ZipInputStream zis = new ZipInputStream(fis); + ZipEntry ze = zis.getNextEntry(); + + while (ze != null) + { + String entryName = ze.getName(); + String[] parts = entryName.split("/"); + + String appenderName = parts[0]; + String logFileName = parts[1]; + + LogFileDetails d = findLogFileDetails(logFileName, appenderName, details); + + assertNotNull("Unexpected entry " + entryName, d); + details.remove(d); + + File logFile = d.getLocation(); + String logContent = FileUtils.readFileAsString(logFile); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len; + while ((len = zis.read(buffer)) > 0) + { + baos.write(buffer, 0, len); + } + baos.close(); + + assertEquals("Unexpected log file content", logContent, baos.toString()); + + ze = zis.getNextEntry(); + } + + zis.closeEntry(); + zis.close(); + + } + finally + { + if (fis != null) + { + fis.close(); + } + } + assertEquals("Not all log files have been output", 0, details.size()); + } + +} diff --git a/java/broker/etc/broker_example.acl b/java/broker/etc/broker_example.acl index 6cab707a89..29dca90f15 100644 --- a/java/broker/etc/broker_example.acl +++ b/java/broker/etc/broker_example.acl @@ -76,6 +76,9 @@ ACL ALLOW-LOG webadmins UPDATE METHOD # authorise operations changing broker model ACL ALLOW-LOG webadmins CONFIGURE BROKER +# authorise operations to view and download broker logs +ACL ALLOW webadmins ACCESS_LOGS BROKER + # at the moment only the following UPDATE METHOD rules are supported by web management console #ACL ALLOW-LOG webadmins UPDATE METHOD component="VirtualHost.Queue" name="moveMessages" #ACL ALLOW-LOG webadmins UPDATE METHOD component="VirtualHost.Queue" name="copyMessages" diff --git a/java/broker/src/main/java/org/apache/qpid/server/configuration/BrokerProperties.java b/java/broker/src/main/java/org/apache/qpid/server/configuration/BrokerProperties.java index 0b31f5b81a..fb382a8ca9 100644 --- a/java/broker/src/main/java/org/apache/qpid/server/configuration/BrokerProperties.java +++ b/java/broker/src/main/java/org/apache/qpid/server/configuration/BrokerProperties.java @@ -54,6 +54,7 @@ public class BrokerProperties public static final String PROPERTY_QPID_HOME = "QPID_HOME"; public static final String PROPERTY_QPID_WORK = "QPID_WORK"; + public static final String PROPERTY_LOG_RECORDS_BUFFER_SIZE = "qpid.broker_log_records_buffer_size"; private BrokerProperties() { diff --git a/java/broker/src/main/java/org/apache/qpid/server/logging/LogRecorder.java b/java/broker/src/main/java/org/apache/qpid/server/logging/LogRecorder.java index 5528a05360..dfffbdbb5f 100644 --- a/java/broker/src/main/java/org/apache/qpid/server/logging/LogRecorder.java +++ b/java/broker/src/main/java/org/apache/qpid/server/logging/LogRecorder.java @@ -25,15 +25,17 @@ import org.apache.log4j.spi.ErrorHandler; import org.apache.log4j.spi.Filter; import org.apache.log4j.spi.LoggingEvent; import org.apache.log4j.spi.ThrowableInformation; +import org.apache.qpid.server.configuration.BrokerProperties; public class LogRecorder implements Appender, Iterable<LogRecorder.Record> { + private static final int DEFAULT_BUFFER_SIZE = 4096; private ErrorHandler _errorHandler; private Filter _filter; private String _name; private long _recordId; - private final int _bufferSize = 4096; + private final int _bufferSize = Integer.getInteger(BrokerProperties.PROPERTY_LOG_RECORDS_BUFFER_SIZE, DEFAULT_BUFFER_SIZE); private final int _mask = _bufferSize - 1; private Record[] _records = new Record[_bufferSize]; diff --git a/java/broker/src/main/java/org/apache/qpid/server/security/SecurityManager.java b/java/broker/src/main/java/org/apache/qpid/server/security/SecurityManager.java index 09e79d3ae9..931368cb97 100755 --- a/java/broker/src/main/java/org/apache/qpid/server/security/SecurityManager.java +++ b/java/broker/src/main/java/org/apache/qpid/server/security/SecurityManager.java @@ -44,6 +44,7 @@ import static org.apache.qpid.server.security.access.ObjectType.METHOD; import static org.apache.qpid.server.security.access.ObjectType.QUEUE; import static org.apache.qpid.server.security.access.ObjectType.USER; import static org.apache.qpid.server.security.access.ObjectType.VIRTUALHOST; +import static org.apache.qpid.server.security.access.Operation.ACCESS_LOGS; import static org.apache.qpid.server.security.access.Operation.BIND; import static org.apache.qpid.server.security.access.Operation.CONFIGURE; import static org.apache.qpid.server.security.access.Operation.CONSUME; @@ -629,4 +630,15 @@ public class SecurityManager implements ConfigurationChangeListener }); } + public boolean authoriseLogsAccess() + { + return checkAllPlugins(new AccessCheck() + { + Result allowed(AccessControl plugin) + { + return plugin.authorise(ACCESS_LOGS, BROKER, ObjectProperties.EMPTY); + } + }); + } + } diff --git a/java/broker/src/main/java/org/apache/qpid/server/security/access/ObjectType.java b/java/broker/src/main/java/org/apache/qpid/server/security/access/ObjectType.java index 048d9a8fc9..9016205d1c 100644 --- a/java/broker/src/main/java/org/apache/qpid/server/security/access/ObjectType.java +++ b/java/broker/src/main/java/org/apache/qpid/server/security/access/ObjectType.java @@ -19,6 +19,7 @@ package org.apache.qpid.server.security.access; import static org.apache.qpid.server.security.access.Operation.ACCESS; +import static org.apache.qpid.server.security.access.Operation.ACCESS_LOGS; import static org.apache.qpid.server.security.access.Operation.BIND; import static org.apache.qpid.server.security.access.Operation.CONFIGURE; import static org.apache.qpid.server.security.access.Operation.CONSUME; @@ -50,7 +51,7 @@ public enum ObjectType METHOD(Operation.ALL, ACCESS, UPDATE), USER(Operation.ALL, CREATE, DELETE, UPDATE), GROUP(Operation.ALL, CREATE, DELETE, UPDATE), - BROKER(Operation.ALL, CONFIGURE); + BROKER(Operation.ALL, CONFIGURE, ACCESS_LOGS); private EnumSet<Operation> _actions; diff --git a/java/broker/src/main/java/org/apache/qpid/server/security/access/Operation.java b/java/broker/src/main/java/org/apache/qpid/server/security/access/Operation.java index df5289b7bf..db5b8fba11 100644 --- a/java/broker/src/main/java/org/apache/qpid/server/security/access/Operation.java +++ b/java/broker/src/main/java/org/apache/qpid/server/security/access/Operation.java @@ -33,7 +33,8 @@ public enum Operation DELETE, PURGE, UPDATE, - CONFIGURE; + CONFIGURE, + ACCESS_LOGS; public static Operation parse(String text) { diff --git a/java/systests/src/main/java/org/apache/qpid/systest/rest/LogRecordsRestTest.java b/java/systests/src/main/java/org/apache/qpid/systest/rest/LogRecordsRestTest.java index a2f9d3189c..8f2c138869 100644 --- a/java/systests/src/main/java/org/apache/qpid/systest/rest/LogRecordsRestTest.java +++ b/java/systests/src/main/java/org/apache/qpid/systest/rest/LogRecordsRestTest.java @@ -39,4 +39,25 @@ public class LogRecordsRestTest extends QpidRestTestCase assertEquals("Unexpected thread", "main", record.get("thread")); assertEquals("Unexpected logger", "qpid.message.broker.ready", record.get("logger")); } + + public void testGetLogsFromGivenId() throws Exception + { + List<Map<String, Object>> logs = getRestTestHelper().getJsonAsList("/rest/logrecords"); + assertNotNull("Logs data cannot be null", logs); + assertTrue("Logs are not found", logs.size() > 0); + + Map<String, Object> lastLog = logs.get(logs.size() -1); + Object lastId = lastLog.get("id"); + + //make sure that new logs are created + getConnection(); + + List<Map<String, Object>> newLogs = getRestTestHelper().getJsonAsList("/rest/logrecords?lastLogId=" + lastId); + assertNotNull("Logs data cannot be null", newLogs); + assertTrue("Logs are not found", newLogs.size() > 0); + + Object nextId = newLogs.get(0).get("id"); + + assertEquals("Unexpected next log id", ((Number)lastId).longValue() + 1, ((Number)nextId).longValue()); + } } diff --git a/java/systests/src/main/java/org/apache/qpid/systest/rest/LogViewerTest.java b/java/systests/src/main/java/org/apache/qpid/systest/rest/LogViewerTest.java new file mode 100644 index 0000000000..6166e8afc1 --- /dev/null +++ b/java/systests/src/main/java/org/apache/qpid/systest/rest/LogViewerTest.java @@ -0,0 +1,105 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.systest.rest; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.apache.qpid.server.BrokerOptions; + +public class LogViewerTest extends QpidRestTestCase +{ + public static final String DEFAULT_FILE_APPENDER_NAME = "FileAppender"; + private String _expectedLogFileName; + + public void setUp() throws Exception + { + setSystemProperty("logsuffix", "-" + getTestQueueName()); + _expectedLogFileName = System.getProperty("logprefix", "") + "qpid" + System.getProperty("logsuffix", "") + ".log"; + + // use real broker log file + File brokerLogFile = new File(System.getProperty(QPID_HOME), BrokerOptions.DEFAULT_LOG_CONFIG_FILE); + setBrokerCommandLog4JFile(brokerLogFile); + + super.setUp(); + } + + public void testGetLogFiles() throws Exception + { + List<Map<String, Object>> logFiles = getRestTestHelper().getJsonAsList("/rest/logfiles"); + assertNotNull("Log files data cannot be null", logFiles); + + // 1 file appender is configured in QPID default log4j xml: + assertEquals("Unexpected number of log files", 1, logFiles.size()); + + Map<String, Object> logFileDetails = logFiles.get(0); + assertEquals("Unexpected log file name", _expectedLogFileName, logFileDetails.get("name")); + assertEquals("Unexpected log file mime type", "text/plain", logFileDetails.get("mimeType")); + assertEquals("Unexpected log file appender",DEFAULT_FILE_APPENDER_NAME, logFileDetails.get("appenderName")); + assertTrue("Unexpected log file size", ((Number)logFileDetails.get("size")).longValue()>0); + assertTrue("Unexpected log file modification time", ((Number)logFileDetails.get("lastModified")).longValue()>0); + } + + public void testDownloadExistingLogFiles() throws Exception + { + byte[] bytes = getRestTestHelper().getBytes("/rest/logfile?l=" + DEFAULT_FILE_APPENDER_NAME + "%2F" + _expectedLogFileName); + + ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(bytes)); + try + { + ZipEntry entry = zis.getNextEntry(); + assertEquals("Unexpected broker log file name", DEFAULT_FILE_APPENDER_NAME + "/" + _expectedLogFileName, entry.getName()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len; + while ((len = zis.read(buffer)) > 0) + { + baos.write(buffer, 0, len); + } + baos.close(); + assertTrue("Unexpected broker log file content", new String(baos.toByteArray()).contains("BRK-1004")); + assertNull("Unexpepected log file entry", zis.getNextEntry()); + } + finally + { + zis.close(); + } + } + + public void testDownloadNonExistingLogFiles() throws Exception + { + int responseCode = getRestTestHelper().submitRequest("/rest/logfile?l=" + DEFAULT_FILE_APPENDER_NAME + "%2F" + + _expectedLogFileName + "_" + System.currentTimeMillis(), "GET", null); + + assertEquals("Unexpected response code", 404, responseCode); + } + + public void testDownloadNonLogFiles() throws Exception + { + int responseCode = getRestTestHelper().submitRequest("/rest/logfile?l=config.json", "GET", null); + assertEquals("Unexpected response code", 400, responseCode); + } +} diff --git a/java/systests/src/main/java/org/apache/qpid/systest/rest/RestTestHelper.java b/java/systests/src/main/java/org/apache/qpid/systest/rest/RestTestHelper.java index c15e5d7285..7d99b30049 100644 --- a/java/systests/src/main/java/org/apache/qpid/systest/rest/RestTestHelper.java +++ b/java/systests/src/main/java/org/apache/qpid/systest/rest/RestTestHelper.java @@ -468,4 +468,11 @@ public class RestTestHelper connection.disconnect(); return responseCode; } + + public byte[] getBytes(String path) throws IOException + { + HttpURLConnection connection = openManagementConnection(path, "GET"); + connection.connect(); + return readConnectionInputStream(connection); + } } diff --git a/java/systests/src/main/java/org/apache/qpid/systest/rest/acl/LogViewerACLTest.java b/java/systests/src/main/java/org/apache/qpid/systest/rest/acl/LogViewerACLTest.java new file mode 100644 index 0000000000..5a2ebe3e8e --- /dev/null +++ b/java/systests/src/main/java/org/apache/qpid/systest/rest/acl/LogViewerACLTest.java @@ -0,0 +1,100 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.qpid.systest.rest.acl; + +import java.io.IOException; + +import org.apache.commons.configuration.ConfigurationException; +import org.apache.qpid.server.management.plugin.HttpManagement; +import org.apache.qpid.server.security.acl.AbstractACLTestCase; +import org.apache.qpid.systest.rest.QpidRestTestCase; +import org.apache.qpid.test.utils.TestBrokerConfiguration; + +public class LogViewerACLTest extends QpidRestTestCase +{ + private static final String ALLOWED_USER = "user1"; + private static final String DENIED_USER = "user2"; + + @Override + protected void customizeConfiguration() throws ConfigurationException, IOException + { + super.customizeConfiguration(); + getRestTestHelper().configureTemporaryPasswordFile(this, ALLOWED_USER, DENIED_USER); + + AbstractACLTestCase.writeACLFileUtil(this, null, + "ACL ALLOW-LOG ALL ACCESS MANAGEMENT", + "ACL ALLOW-LOG " + ALLOWED_USER + " ACCESS_LOGS BROKER", + "ACL DENY-LOG " + DENIED_USER + " ACCESS_LOGS BROKER", + "ACL DENY-LOG ALL ALL"); + + getBrokerConfiguration().setObjectAttribute(TestBrokerConfiguration.ENTRY_NAME_HTTP_MANAGEMENT, + HttpManagement.HTTP_BASIC_AUTHENTICATION_ENABLED, true); + } + + public void testGetLogRecordsAllowed() throws Exception + { + getRestTestHelper().setUsernameAndPassword(ALLOWED_USER, ALLOWED_USER); + + int responseCode = getRestTestHelper().submitRequest("/rest/logrecords", "GET", null); + assertEquals("Access to log records should be allowed", 200, responseCode); + } + + public void testGetLogRecordsDenied() throws Exception + { + getRestTestHelper().setUsernameAndPassword(DENIED_USER, DENIED_USER); + + int responseCode = getRestTestHelper().submitRequest("/rest/logrecords", "GET", null); + assertEquals("Access to log records should be denied", 403, responseCode); + } + + public void testGetLogFilesAllowed() throws Exception + { + getRestTestHelper().setUsernameAndPassword(ALLOWED_USER, ALLOWED_USER); + + int responseCode = getRestTestHelper().submitRequest("/rest/logfiles", "GET", null); + assertEquals("Access to log files should be allowed", 200, responseCode); + } + + public void testGetLogFilesDenied() throws Exception + { + getRestTestHelper().setUsernameAndPassword(DENIED_USER, DENIED_USER); + + int responseCode = getRestTestHelper().submitRequest("/rest/logfiles", "GET", null); + assertEquals("Access to log files should be denied", 403, responseCode); + } + + public void testDownloadLogFileAllowed() throws Exception + { + getRestTestHelper().setUsernameAndPassword(ALLOWED_USER, ALLOWED_USER); + + int responseCode = getRestTestHelper().submitRequest("/rest/logfile?l=appender%2fqpid.log", "GET", null); + assertEquals("Access to log files should be allowed", 404, responseCode); + } + + public void testDownloadLogFileDenied() throws Exception + { + getRestTestHelper().setUsernameAndPassword(DENIED_USER, DENIED_USER); + + int responseCode = getRestTestHelper().submitRequest("/rest/logfile?l=appender%2fqpid.log", "GET", null); + assertEquals("Access to log files should be denied", 403, responseCode); + } + +} |