You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iotdb.apache.org by ji...@apache.org on 2019/08/27 03:03:53 UTC

[incubator-iotdb] branch log_tool updated: add basic functionalities

This is an automated email from the ASF dual-hosted git repository.

jiangtian pushed a commit to branch log_tool
in repository https://gitbox.apache.org/repos/asf/incubator-iotdb.git


The following commit(s) were added to refs/heads/log_tool by this push:
     new 9dd65e6  add basic functionalities
9dd65e6 is described below

commit 9dd65e6d05d3c32dc830431faa7c2682fcb93710
Author: jt <jt...@163.com>
AuthorDate: Tue Aug 27 10:53:59 2019 +0800

    add basic functionalities
---
 .../resources/tools/logAnalyze/config.properties   |  22 +++
 .../resources/tools/logAnalyze/default.log.pattern |  19 ++
 .../logAnalyze/plans/flushTimeConsumption.plan     |  19 ++
 .../apache/iotdb/db/tools/logvisual/LogEntry.java  |   2 +-
 .../apache/iotdb/db/tools/logvisual/LogFilter.java | 131 ++++++++++---
 .../iotdb/db/tools/logvisual/LogVisualizer.java    | 155 ++++++++++-----
 .../iotdb/db/tools/logvisual/PatternLogParser.java |   2 +-
 .../db/tools/logvisual/TimeSeriesStatistics.java   | 109 +++++++++++
 .../iotdb/db/tools/logvisual/VisualUtils.java      |  50 +++++
 .../{VisualizePlan.java => VisualizationPlan.java} |  90 ++++++---
 .../db/tools/logvisual/conf/PropertyKeys.java      |  34 ++++
 .../db/tools/logvisual/gui/ClosableComboTab.java   |  39 ++++
 .../iotdb/db/tools/logvisual/gui/ClosableTab.java  |  52 +++++
 .../db/tools/logvisual/gui/FileSelectionBox.java   |  90 +++++++++
 .../db/tools/logvisual/gui/LabeledComboBox.java    |  59 ++++++
 .../iotdb/db/tools/logvisual/gui/LoadLogBox.java   |  61 ++++++
 .../db/tools/logvisual/gui/LogVisualizeGui.java    | 128 ++++++++++++
 .../iotdb/db/tools/logvisual/gui/MainPanel.java    |  87 +++++++++
 .../iotdb/db/tools/logvisual/gui/PlanBox.java      | 189 ++++++++++++++++++
 .../db/tools/logvisual/gui/PlanDetailPanel.java    | 216 +++++++++++++++++++++
 .../db/tools/logvisual/gui/ResultPlotTab.java      |  45 +++++
 .../db/tools/logvisual/gui/ResultStatisticTab.java |  70 +++++++
 22 files changed, 1575 insertions(+), 94 deletions(-)

diff --git a/server/src/assembly/resources/tools/logAnalyze/config.properties b/server/src/assembly/resources/tools/logAnalyze/config.properties
new file mode 100644
index 0000000..a7cddc4
--- /dev/null
+++ b/server/src/assembly/resources/tools/logAnalyze/config.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+parser_properties_path="C:\\Users\\admin\\Desktop\\logs\\logAnalyze\\default.log.pattern"
+plans_path="C:\\Users\\admin\\Desktop\\logs\\logAnalyze\\plans"
+log_path="C:\\Users\\admin\\Desktop\\logs\\log_all.log"
\ No newline at end of file
diff --git a/server/src/assembly/resources/tools/logAnalyze/default.log.pattern b/server/src/assembly/resources/tools/logAnalyze/default.log.pattern
index fccbd91..9cf7f9f 100644
--- a/server/src/assembly/resources/tools/logAnalyze/default.log.pattern
+++ b/server/src/assembly/resources/tools/logAnalyze/default.log.pattern
@@ -1,3 +1,22 @@
+#
+# 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.
+#
+
 pattern=([^\\[]*)(\\[.*])(\\s\\w+\\s)([^:]*:\\d+)(\\s-\\s)(.*)
 date_index=1
 thread_name_index=2
diff --git a/server/src/assembly/resources/tools/logAnalyze/plans/flushTimeConsumption.plan b/server/src/assembly/resources/tools/logAnalyze/plans/flushTimeConsumption.plan
index f18e0d6..ccf1b11 100644
--- a/server/src/assembly/resources/tools/logAnalyze/plans/flushTimeConsumption.plan
+++ b/server/src/assembly/resources/tools/logAnalyze/plans/flushTimeConsumption.plan
@@ -1,3 +1,22 @@
+#
+# 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.
+#
+
 name=flushTimeConsumption
 content_pattern=Storage group (.*) memtable (.*) flushing a memtable has finished! Time consumption: (\\d+)ms
 
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/LogEntry.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/LogEntry.java
index 7146113..18b7634 100644
--- a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/LogEntry.java
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/LogEntry.java
@@ -61,7 +61,7 @@ public class LogEntry {
     this.measurements = measurements;
   }
 
-  enum LogLevel {
+  public enum LogLevel {
     DEBUG, INFO, WARN, ERROR
   }
 
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/LogFilter.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/LogFilter.java
index 054fa6e..8b2456a 100644
--- a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/LogFilter.java
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/LogFilter.java
@@ -1,16 +1,12 @@
 package org.apache.iotdb.db.tools.logvisual;
 
 import static org.apache.iotdb.db.tools.logvisual.LogFilter.FilterProperties.*;
-import static org.apache.iotdb.db.tools.logvisual.PatternLogParser.PatternProperties.DATE_PATTERN;
 
 import java.io.IOException;
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Date;
-import java.util.List;
 import java.util.Properties;
 import org.apache.iotdb.db.tools.logvisual.LogEntry.LogLevel;
 
@@ -19,9 +15,9 @@ public class LogFilter {
   private LogLevel minLevel = LogLevel.DEBUG;
   // optional, only threads, classes, lines in the lists are analyzed. When unset, all logs will
   // analyzed. comma-separated
-  private List<String> threadNameWhiteList;
-  private List<String> classNameWhiteList;
-  private List<Integer> lineNumWhiteList;
+  private String[] threadNameWhiteList;
+  private String[] classNameWhiteList;
+  private int[] lineNumWhiteList;
   // optional, only time ranges within the interval will be analyzed
   // if startDate or endDate is set, datePattern must be set too
   private DateFormat datePartten;
@@ -34,22 +30,16 @@ public class LogFilter {
     String threadNameWhiteListStr = properties.getProperty(THREAD_NAME_WHITE_LIST.getPropertyName
         ());
     if (threadNameWhiteListStr != null) {
-      threadNameWhiteList = Arrays.asList(threadNameWhiteListStr.trim().split(","));
+      threadNameWhiteList = threadNameWhiteListStr.trim().split(",");
     }
 
     String classNameWhiteListStr = properties.getProperty(CLASS_NAME_WHITE_LIST.getPropertyName());
     if (classNameWhiteListStr != null) {
-      classNameWhiteList = Arrays.asList(classNameWhiteListStr.trim().split(","));
+      classNameWhiteList =classNameWhiteListStr.trim().split(",");
     }
 
-    String lineNumWhiteListStr = properties.getProperty(LINE_NUM_WHITE_LIST.getPropertyName());
-    if (lineNumWhiteListStr != null) {
-      String[] lineNumWhiteListStrs = lineNumWhiteListStr.trim().split(",");
-      lineNumWhiteList = new ArrayList<>();
-      for (int i = 0; i < lineNumWhiteListStrs.length; i++) {
-        lineNumWhiteList.add(Integer.parseInt(lineNumWhiteListStrs[i]));
-      }
-    }
+    lineNumWhiteList = VisualUtils.parseIntArray(properties.getProperty(LINE_NUM_WHITE_LIST
+        .getPropertyName()));
 
     String datePatternStr = properties.getProperty(DATE_PATTERN.getPropertyName());
     if (datePatternStr != null) {
@@ -76,11 +66,11 @@ public class LogFilter {
 
   public FilterFeedBack filter(LogEntry entry) {
    if (entry.getLogLevel().ordinal() < minLevel.ordinal() ||
-       (threadNameWhiteList != null && !threadNameWhiteList.contains(entry.getThreadName())) ||
-       (classNameWhiteList != null && !classNameWhiteList.contains(entry.getCodeLocation()
+       (threadNameWhiteList != null && !strsContains(threadNameWhiteList, entry.getThreadName())) ||
+       (classNameWhiteList != null && !strsContains(classNameWhiteList, entry.getCodeLocation()
            .getClassName())) ||
-       (lineNumWhiteList != null && !lineNumWhiteList.contains(entry.getCodeLocation().getLineNum
-           ())) ||
+       (lineNumWhiteList != null && !intsContains(lineNumWhiteList, entry.getCodeLocation()
+           .getLineNum())) ||
        (startDate != null && entry.getDate().before(startDate))) {
      return FilterFeedBack.REJECT;
    }
@@ -91,7 +81,104 @@ public class LogFilter {
     return FilterFeedBack.OK;
   }
 
-  enum FilterProperties {
+  private boolean strsContains(String[] strings, String target) {
+    for (String str : strings) {
+      if (str.equals(target)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean intsContains(int[] ints, int target) {
+    for (int i : ints) {
+      if (i == target) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public LogLevel getMinLevel() {
+    return minLevel;
+  }
+
+  public String[] getThreadNameWhiteList() {
+    return threadNameWhiteList;
+  }
+
+  public String[] getClassNameWhiteList() {
+    return classNameWhiteList;
+  }
+
+  public int[] getLineNumWhiteList() {
+    return lineNumWhiteList;
+  }
+
+  public DateFormat getDatePatten() {
+    return datePartten;
+  }
+
+  public Date getStartDate() {
+    return startDate;
+  }
+
+  public Date getEndDate() {
+    return endDate;
+  }
+
+  public void setMinLevel(LogLevel minLevel) {
+    this.minLevel = minLevel;
+  }
+
+  public void setThreadNameWhiteList(String[] threadNameWhiteList) {
+    this.threadNameWhiteList = threadNameWhiteList;
+  }
+
+  public void setClassNameWhiteList(String[] classNameWhiteList) {
+    this.classNameWhiteList = classNameWhiteList;
+  }
+
+  public void setLineNumWhiteList(int[] lineNumWhiteList) {
+    this.lineNumWhiteList = lineNumWhiteList;
+  }
+
+  public void setDatePartten(DateFormat datePartten) {
+    this.datePartten = datePartten;
+  }
+
+  public void setStartDate(Date startDate) {
+    this.startDate = startDate;
+  }
+
+  public void setEndDate(Date endDate) {
+    this.endDate = endDate;
+  }
+
+  public void saveIntoProperties(Properties properties) {
+    properties.put(MIN_LEVEL.propertyName, minLevel.toString());
+    if (threadNameWhiteList != null) {
+      properties.put(THREAD_NAME_WHITE_LIST.propertyName, String.join(",", threadNameWhiteList));
+    }
+    if (classNameWhiteList != null) {
+      properties.put(CLASS_NAME_WHITE_LIST.propertyName, String.join(",", classNameWhiteList));
+    }
+    if (lineNumWhiteList != null) {
+      properties.put(LINE_NUM_WHITE_LIST.propertyName, VisualUtils.intArrayToString
+          (lineNumWhiteList));
+    }
+    if (startDate != null) {
+      properties.put(START_DATE.propertyName, datePartten.format(startDate));
+    }
+    if (endDate != null) {
+      properties.put(END_DATE.propertyName, datePartten.format(endDate));
+    }
+    if (datePartten != null) {
+      properties.put(DATE_PATTERN.propertyName, ((SimpleDateFormat) datePartten).toPattern());
+    }
+  }
+
+  public enum FilterProperties {
     MIN_LEVEL("min_level"), THREAD_NAME_WHITE_LIST("thread_name_white_list"), CLASS_NAME_WHITE_LIST(
         "class_name_white_list"),
     LINE_NUM_WHITE_LIST("line_num_white_list"), START_DATE("start_date"), END_DATE("end_date"),
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/LogVisualizer.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/LogVisualizer.java
index bf9b660..f897fb1 100644
--- a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/LogVisualizer.java
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/LogVisualizer.java
@@ -1,19 +1,17 @@
 package org.apache.iotdb.db.tools.logvisual;
 
-import java.awt.Graphics;
-import java.awt.image.BufferedImage;
 import java.io.BufferedInputStream;
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
-import javax.swing.JFrame;
-import javax.swing.JPanel;
 import org.apache.iotdb.db.tools.logvisual.exceptions.AnalyzeException;
 import org.apache.iotdb.db.tools.logvisual.exceptions.NoLogFileLoadedException;
 import org.apache.iotdb.db.tools.logvisual.exceptions.NoSuchPlanException;
@@ -31,21 +29,33 @@ import org.slf4j.LoggerFactory;
 public class LogVisualizer {
 
   private static final Logger logger = LoggerFactory.getLogger(LogVisualizer.class);
-  private static final String STR_HAPPENED_INSTANCE = "HappenedInstance";
 
   private LogParser logParser;
   private List<LogEntry> logCache = new ArrayList<>();
 
-  private Map<String, VisualizePlan> plans = new HashMap<>();
+  private Map<String, VisualizationPlan> plans = new HashMap<>();
   private Map<List<String>, List<LogEntry>> logGroups = new HashMap<>();
 
+  private Map<String, JFreeChart> charts;
+  private Map<String, List<TimeSeriesStatistics>> statisticsMap;
+
+  private File parserPropertyFile;
+  private File logFile;
+
   private void clearLogs() {
     logCache.clear();
     logGroups.clear();
   }
 
-  public void loadLogParser(String propertyFilePath, String logFilePath)
-      throws IOException {
+  public void loadLogParser() throws IOException {
+    if (parserPropertyFile == null) {
+      throw new IOException("Parser property file unset!");
+    }
+    if (logFile == null) {
+      throw new IOException("Log file unset!");
+    }
+    String propertyFilePath = parserPropertyFile.getPath();
+    String logFilePath = logFile.getPath();
     Properties properties = new Properties();
     try (FileInputStream inputStream = new FileInputStream(propertyFilePath);
         BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream)) {
@@ -59,25 +69,41 @@ public class LogVisualizer {
     logParser = null;
   }
 
-  public void loadPlan(String planFilePath) throws IOException {
-    try (FileInputStream reader = new FileInputStream(planFilePath);
-        BufferedInputStream bufferedInputStream = new BufferedInputStream(reader)) {
-      Properties properties = new Properties();
-      properties.load(bufferedInputStream);
-      VisualizePlan plan = new VisualizePlan(properties);
-      plans.put(plan.getName(), plan);
+  public void loadPlans(File[] planFiles) throws IOException {
+    for (File file : planFiles) {
+      loadPlan(file);
+    }
+  }
+
+  public void loadPlan(File file) throws IOException {
+    if (!file.exists()) {
+      return;
+    }
+    if (file.isDirectory()) {
+      loadPlans(file.listFiles());
+    } else {
+      loadPlan(file.getPath());
     }
   }
 
-  public Collection<String> listPlans() {
-    return plans.keySet();
+  private void loadPlan(String planFilePath) throws IOException {
+    VisualizationPlan plan = new VisualizationPlan(planFilePath);
+    plans.put(plan.getName(), plan);
+  }
+
+  public Collection<VisualizationPlan> listPlans() {
+    return plans.values();
   }
 
   public void executePlan(String planName) throws AnalyzeException {
-    VisualizePlan plan = plans.get(planName);
+    VisualizationPlan plan = plans.get(planName);
     if (plan == null) {
       throw new NoSuchPlanException(planName);
     }
+    executePlan(plan);
+  }
+
+  public void executePlan(VisualizationPlan plan) throws AnalyzeException {
     if (logParser == null) {
       throw new NoLogFileLoadedException();
     }
@@ -89,11 +115,11 @@ public class LogVisualizer {
     collectLogs(plan);
     groupLogs();
     Map<String,TimeSeriesCollection> taggedTimeSeries = createTimeSeries(plan);
-    List<JFreeChart> charts = drawCharts(taggedTimeSeries, plan);
-    showCharts(charts.subList(0,10));
+    charts = drawCharts(taggedTimeSeries, plan);
+    statisticsMap = genStatisticMap(taggedTimeSeries);
   }
 
-  private void collectLogs(VisualizePlan plan) throws AnalyzeException {
+  private void collectLogs(VisualizationPlan plan) throws AnalyzeException {
     LogFilter logFilter = plan.getLogFilter();
     clearLogs();
     try {
@@ -128,7 +154,7 @@ public class LogVisualizer {
     logger.info("Found {} different tags", logGroups.size());
   }
 
-  private Map<String, TimeSeriesCollection> createTimeSeries(VisualizePlan plan) {
+  private Map<String, TimeSeriesCollection> createTimeSeries(VisualizationPlan plan) {
     Map<String, TimeSeriesCollection> ret = new HashMap<>();
     for (Entry<List<String>, List<LogEntry>> entry : logGroups.entrySet()) {
       List<String> tags = entry.getKey();
@@ -148,7 +174,8 @@ public class LogVisualizer {
       if (plan.getMeasurementPositions() != null) {
         String[] legends = plan.getLegends();
         for (String legend : legends) {
-          tagTimeseries.addSeries(new TimeSeries(concatenatedTag + legend));
+          TimeSeries timeSeries = new TimeSeries(concatenatedTag + legend);
+          tagTimeseries.addSeries(timeSeries);
         }
         for (LogEntry logEntry : logs) {
           List<Double> values = logEntry.getMeasurements();
@@ -159,54 +186,88 @@ public class LogVisualizer {
       } else {
         TimeSeries happenedInstance = new TimeSeries("HappenedInstance");
         for (LogEntry logEntry : logs) {
-          happenedInstance.add(new Millisecond(logEntry.getDate()), 1.0);
+          happenedInstance.addOrUpdate(new Millisecond(logEntry.getDate()), 1.0);
         }
+        tagTimeseries.addSeries(happenedInstance);
       }
       ret.put(concatenatedTag, tagTimeseries);
     }
     return ret;
   }
 
-  private List<JFreeChart> drawCharts(Map<String, TimeSeriesCollection> taggedTimeSeries, VisualizePlan plan) {
-    List<JFreeChart> charts = new ArrayList<>();
+  private Map<String, JFreeChart> drawCharts(Map<String, TimeSeriesCollection> taggedTimeSeries,
+      VisualizationPlan plan) {
+    Map<String, JFreeChart> charts = new HashMap<>();
     for (Entry<String, TimeSeriesCollection> entry : taggedTimeSeries.entrySet()) {
       String tag = entry.getKey();
       TimeSeriesCollection timeSeriesList = entry.getValue();
+      Date startDate = new Date((long) timeSeriesList.getDomainBounds(true).getLowerBound());
       if (plan.getMeasurementPositions() != null) {
         // a real-valued timeseries, draw a curve
-        JFreeChart chart = ChartFactory.createTimeSeriesChart(tag, "time", "value", timeSeriesList);
+        JFreeChart chart = ChartFactory.createTimeSeriesChart(tag, "time-"+ startDate, "value",
+            timeSeriesList);
         XYPlot xyPlot = chart.getXYPlot();
-        ((XYLineAndShapeRenderer) xyPlot.getRenderer()).setDefaultShapesFilled(true);
-        charts.add(chart);
+        XYLineAndShapeRenderer xyLineAndShapeRenderer = ((XYLineAndShapeRenderer) xyPlot
+            .getRenderer());
+        xyLineAndShapeRenderer.setDefaultShapesVisible(true);
+        xyLineAndShapeRenderer.setDefaultShapesFilled(true);
+
+        charts.put(tag, chart);
       } else {
-        // a binary timeseries, draw a scatter plot
-        JFreeChart chart = ChartFactory.createScatterPlot(tag, "time", "", timeSeriesList);
-        charts.add(chart);
+        JFreeChart chart = ChartFactory.createTimeSeriesChart(tag, "time-"+ startDate, "value",
+            timeSeriesList);
+        XYPlot xyPlot = chart.getXYPlot();
+        XYLineAndShapeRenderer xyLineAndShapeRenderer = ((XYLineAndShapeRenderer) xyPlot
+            .getRenderer());
+        xyLineAndShapeRenderer.setDefaultShapesVisible(true);
+        xyLineAndShapeRenderer.setDefaultShapesFilled(true);
+        xyLineAndShapeRenderer.setDefaultLinesVisible(false);
+
+        charts.put(tag, chart);
       }
     }
     return charts;
   }
 
-  private void showCharts(List<JFreeChart> charts) {
-    for (JFreeChart chart : charts) {
-      BufferedImage image = chart.createBufferedImage(800, 600);
-      JFrame frame = new JFrame();
-      frame.add(new JPanel() {
-        @Override
-        protected void paintComponent(Graphics g) {
-          g.drawImage(image, 0, 0, null);
-        }
-      });
-      frame.setVisible(true);
-      frame.setSize(880, 660);
+  public Map<String, JFreeChart> getCharts() {
+    return charts;
+  }
+
+  public void setParserPropertyFile(File parserPropertyFile) {
+    this.parserPropertyFile = parserPropertyFile;
+  }
+
+  public void setLogFile(File logFile) {
+    this.logFile = logFile;
+  }
+
+  private Map<String, List<TimeSeriesStatistics>> genStatisticMap(Map<String,TimeSeriesCollection>
+      taggedTimeSeries) {
+    Map<String, List<TimeSeriesStatistics>> ret = new HashMap<>();
+    for (Entry<String, TimeSeriesCollection> timeSeriesCollectionEntry : taggedTimeSeries.entrySet()) {
+      String tag = timeSeriesCollectionEntry.getKey();
+      TimeSeriesCollection timeSeriesCollection = timeSeriesCollectionEntry.getValue();
+      List<TimeSeriesStatistics> seriesStatistics = new ArrayList<>();
+      for (int i = 0; i < timeSeriesCollection.getSeriesCount(); i++) {
+        TimeSeries timeSeries = timeSeriesCollection.getSeries(i);
+        seriesStatistics.add(new TimeSeriesStatistics(timeSeries));
+      }
+      ret.put(tag, seriesStatistics);
     }
+    return ret;
+  }
+
+  public Map<String, List<TimeSeriesStatistics>> getStatisticsMap() {
+    return statisticsMap;
   }
 
   public static void main(String[] args) throws IOException, AnalyzeException {
     LogVisualizer visualizer = new LogVisualizer();
-    visualizer.loadLogParser("E:\\codestore\\incubator-iotdb\\server\\src\\assembly\\resources"
-        + "\\tools\\logAnalyze\\default.log.pattern",
-        "C:\\Users\\admin\\Desktop\\logs\\log-all-2019-08-21.0.log");
+    visualizer.setParserPropertyFile(new File("E:\\codestore\\incubator-iotdb\\server\\src\\assembly"
+        + "\\resources"
+        + "\\tools\\logAnalyze\\default.log.pattern"));
+    visualizer.setLogFile( new File("C:\\Users\\admin\\Desktop\\logs\\log-all-2019-08-21.0.log"));
+    visualizer.loadLogParser();
     visualizer.loadPlan("E:\\codestore\\incubator-iotdb\\server\\src\\assembly\\resources\\tools\\logAnalyze\\plans\\flushTimeConsumption.plan");
     visualizer.executePlan("flushTimeConsumption");
   }
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/PatternLogParser.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/PatternLogParser.java
index 93d793c..68021cf 100644
--- a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/PatternLogParser.java
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/PatternLogParser.java
@@ -38,7 +38,7 @@ public class PatternLogParser implements LogParser{
   private BufferedReader reader;
   private DateFormat dateFormat;
 
-  PatternLogParser(Properties properties, String logFilePath) throws IOException {
+  PatternLogParser(Properties properties, String logFilePath) {
     this.pattern = Pattern.compile(properties.getProperty(PATTERN.getPropertyName()));
     this.dateIndex = Integer.parseInt(properties.getProperty(DATE_INDEX.getPropertyName()));
     this.threadNameIndex = Integer.parseInt(properties.getProperty(THREAD_NAME_INDEX
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/TimeSeriesStatistics.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/TimeSeriesStatistics.java
new file mode 100644
index 0000000..1bdcdee
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/TimeSeriesStatistics.java
@@ -0,0 +1,109 @@
+/*
+ * 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.iotdb.db.tools.logvisual;
+
+import java.util.Date;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesDataItem;
+
+public class TimeSeriesStatistics {
+
+  public static final String[] HEADER = new String[] {
+      "name", "count", "meanInterval", "maxInterval", "minInterval", "meanVal", "maxVal", "minVal"
+  };
+
+  private String name;
+  private int size = 0;
+  private double meanInterval = 0.0;
+  private long maxInterval = Long.MIN_VALUE;
+  private long minInterval = Long.MAX_VALUE;
+  private double meanVal = 0.0;
+  private double maxVal = Double.MIN_VALUE;
+  private double minVal = Double.MAX_VALUE;
+
+  TimeSeriesStatistics(TimeSeries timeSeries) {
+    Date lastDate = null;
+    name = (String) timeSeries.getKey();
+    for (int i = 0; i < timeSeries.getItemCount(); i++) {
+      TimeSeriesDataItem dataItem = timeSeries.getDataItem(i);
+      Date currDate = dataItem.getPeriod().getStart();
+      double value = dataItem.getValue().doubleValue();
+      if (lastDate == null) {
+        lastDate = currDate;
+      } else {
+        long interval = currDate.getTime() - lastDate.getTime();
+        lastDate = currDate;
+        meanInterval = (meanInterval * size + interval) / (size + 1);
+        maxInterval = maxInterval < interval ? interval : maxInterval;
+        minInterval = minInterval < interval ? minInterval : interval;
+      }
+      meanVal = (meanVal * size + value) / (size + 1);
+      maxVal = maxVal < value ? value : maxVal;
+      minVal = minVal < value ? minVal : value;
+      size ++;
+    }
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public int getSize() {
+    return size;
+  }
+
+  public double getMeanInterval() {
+    return meanInterval;
+  }
+
+  public long getMaxInterval() {
+    return maxInterval;
+  }
+
+  public long getMinInterval() {
+    return minInterval;
+  }
+
+  public double getMeanVal() {
+    return meanVal;
+  }
+
+  public double getMaxVal() {
+    return maxVal;
+  }
+
+  public double getMinVal() {
+    return minVal;
+  }
+
+  public Object[] toArray() {
+    Object[] ret = new Object[HEADER.length];
+    int i = 0;
+    ret[i++] = name;
+    ret[i++] = size;
+    ret[i++] = meanInterval;
+    ret[i++] = maxInterval;
+    ret[i++] = minInterval;
+    ret[i++] = meanVal;
+    ret[i++] = maxVal;
+    ret[i] = minVal;
+    return ret;
+  }
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/VisualUtils.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/VisualUtils.java
new file mode 100644
index 0000000..cc56f69
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/VisualUtils.java
@@ -0,0 +1,50 @@
+/*
+ * 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.iotdb.db.tools.logvisual;
+
+public class VisualUtils {
+
+  private VisualUtils() {
+    throw new UnsupportedOperationException("Initializing a util class");
+  }
+
+  public static int[] parseIntArray(String intarrayStr) {
+    if (intarrayStr != null) {
+      String[] intStrs = intarrayStr.split(",");
+      int[] ints = new int[intStrs.length];
+      for (int i = 0; i < ints.length; i++) {
+        ints[i] = Integer.parseInt(intStrs[i]);
+      }
+      return ints;
+    }
+    return null;
+  }
+
+  public static String intArrayToString(int[] ints) {
+    if (ints == null) {
+      return null;
+    }
+    StringBuilder builder = new StringBuilder(String.valueOf(ints[0]));
+    for (int i = 1; i < ints.length; i ++) {
+      builder.append(",").append(ints[i]);
+    }
+    return builder.toString();
+  }
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/VisualizePlan.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/VisualizationPlan.java
similarity index 60%
rename from server/src/main/java/org/apache/iotdb/db/tools/logvisual/VisualizePlan.java
rename to server/src/main/java/org/apache/iotdb/db/tools/logvisual/VisualizationPlan.java
index 431f305..de2a43c 100644
--- a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/VisualizePlan.java
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/VisualizationPlan.java
@@ -1,8 +1,11 @@
 package org.apache.iotdb.db.tools.logvisual;
 
-import static org.apache.iotdb.db.tools.logvisual.VisualizePlan.PlanProperties.*;
+import static org.apache.iotdb.db.tools.logvisual.VisualizationPlan.PlanProperties.*;
 
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
 import java.io.FileInputStream;
+import java.io.FileWriter;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -11,7 +14,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.apache.iotdb.db.tools.logvisual.exceptions.UnmatchedContentException;
 
-public class VisualizePlan {
+public class VisualizationPlan {
   // optional, this will be used as the title of the figure.
   private String name;
 
@@ -36,7 +39,16 @@ public class VisualizePlan {
 
   private LogFilter logFilter;
 
-  public VisualizePlan(Properties properties) throws IOException {
+  private String planFilePath;
+
+  VisualizationPlan(String planFilePath) throws IOException {
+    this.planFilePath = planFilePath;
+    Properties properties = new Properties();
+    try (FileInputStream reader = new FileInputStream(planFilePath);
+        BufferedInputStream bufferedInputStream = new BufferedInputStream(reader)) {
+      properties.load(bufferedInputStream);
+    }
+
     this.name = properties.getProperty(NAME.getPropertyName(), "untitled");
     String patternStr = properties.getProperty(CONTENT_PATTERN.getPropertyName());
     if (patternStr == null) {
@@ -44,28 +56,15 @@ public class VisualizePlan {
     }
     this.contentPattern = Pattern.compile(patternStr);
 
-    String measurementPositionsStr = properties.getProperty(MEASUREMENT_POSITIONS.getPropertyName());
-    if (measurementPositionsStr != null) {
-      String[] measurePosStrs = measurementPositionsStr.split(",");
-      measurementPositions = new int[measurePosStrs.length];
-      for (int i = 0; i < measurementPositions.length; i++) {
-        measurementPositions[i] = Integer.parseInt(measurePosStrs[i]);
-      }
-    }
+    measurementPositions = VisualUtils.parseIntArray(properties.getProperty(MEASUREMENT_POSITIONS
+        .getPropertyName()));
 
     String legendStr = properties.getProperty(LEGENDS.getPropertyName());
     if (legendStr != null) {
       legends = legendStr.split(",");
     }
 
-    String groupByPositionsStr = properties.getProperty(TAG_POSITIONS.getPropertyName());
-    if (groupByPositionsStr != null) {
-      String[] groupByPosStrs = groupByPositionsStr.split(",");
-      tagPositions = new int[groupByPosStrs.length];
-      for (int i = 0; i < tagPositions.length; i++) {
-        tagPositions[i] = Integer.parseInt(groupByPosStrs[i]);
-      }
-    }
+    tagPositions = VisualUtils.parseIntArray(properties.getProperty(TAG_POSITIONS.getPropertyName()));
 
     logFilter = new LogFilter(properties);
   }
@@ -119,6 +118,26 @@ public class VisualizePlan {
     return logFilter;
   }
 
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public void setContentPattern(Pattern contentPattern) {
+    this.contentPattern = contentPattern;
+  }
+
+  public void setMeasurementPositions(int[] measurementPositions) {
+    this.measurementPositions = measurementPositions;
+  }
+
+  public void setLegends(String[] legends) {
+    this.legends = legends;
+  }
+
+  public void setTagPositions(int[] tagPositions) {
+    this.tagPositions = tagPositions;
+  }
+
   enum PlanProperties {
     NAME("name"), CONTENT_PATTERN("content_pattern"), MEASUREMENT_POSITIONS(
         "measurement_positions"),
@@ -135,10 +154,35 @@ public class VisualizePlan {
     }
   }
 
-  public static void main(String[] args) throws IOException {
-    Properties properties = new Properties();
-    properties.load(new FileInputStream("E:\\codestore\\incubator-iotdb\\server\\src\\assembly\\resources\\tools\\logAnalyze\\plans\\flushTimeConsumption.plan"));
 
-    VisualizePlan plan = new VisualizePlan(properties);
+  @Override
+  public String toString() {
+    return name;
+  }
+
+  public void saveAsFile() throws IOException {
+    Properties properties = saveAsProperties();
+    logFilter.saveIntoProperties(properties);
+    try (FileWriter fileWriter = new FileWriter(planFilePath);
+        BufferedWriter bufferedWriter = new BufferedWriter(fileWriter)) {
+      properties.store(bufferedWriter, "");
+    }
+  }
+
+  private Properties saveAsProperties() {
+    Properties properties = new Properties();
+    properties.put(PlanProperties.NAME.getPropertyName(), name);
+    properties.put(PlanProperties.CONTENT_PATTERN.getPropertyName(), contentPattern.pattern());
+    if (measurementPositions != null) {
+      properties.put(PlanProperties.MEASUREMENT_POSITIONS.getPropertyName(), VisualUtils.intArrayToString
+          (measurementPositions));
+    }
+    if (legends != null) {
+      properties.put(PlanProperties.LEGENDS.getPropertyName(), String.join(",", legends));
+    }
+    if (tagPositions != null) {
+      properties.put(PlanProperties.TAG_POSITIONS.getPropertyName(), VisualUtils.intArrayToString(tagPositions));
+    }
+    return properties;
   }
 }
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/conf/PropertyKeys.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/conf/PropertyKeys.java
new file mode 100644
index 0000000..33a42ac
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/conf/PropertyKeys.java
@@ -0,0 +1,34 @@
+/*
+ * 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.iotdb.db.tools.logvisual.conf;
+
+public enum  PropertyKeys {
+  DEFAULT_PARSER_FILE_PATH("parser_properties_path"), DEFAULT_LOG_FILE_PATH
+      ("log_path"), DEFAULT_PLAN_PATH("plans_path");
+  private String key;
+
+  PropertyKeys(String key) {
+    this.key = key;
+  }
+
+  public String getKey() {
+    return key;
+  }
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/ClosableComboTab.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/ClosableComboTab.java
new file mode 100644
index 0000000..227f973
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/ClosableComboTab.java
@@ -0,0 +1,39 @@
+/*
+ * 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.iotdb.db.tools.logvisual.gui;
+
+import java.util.Map;
+
+public abstract class ClosableComboTab extends ClosableTab {
+
+  private LabeledComboBox comboBox;
+
+  public ClosableComboTab(String name, Map comboItems, TabCloseCallBack tabCloseCallBack) {
+    super(name, tabCloseCallBack);
+
+    comboBox = new LabeledComboBox(comboItems, this::onItemSelected, "Please select an item to "
+        + "view");
+    comboBox.setLocation(0, 10);
+    comboBox.setSize(650, 50);
+    add(comboBox);
+  }
+
+  abstract void onItemSelected(Object object);
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/ClosableTab.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/ClosableTab.java
new file mode 100644
index 0000000..956d974
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/ClosableTab.java
@@ -0,0 +1,52 @@
+/*
+ * 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.iotdb.db.tools.logvisual.gui;
+
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import javax.swing.AbstractAction;
+import javax.swing.JButton;
+import javax.swing.JPanel;
+
+abstract class ClosableTab extends JPanel {
+
+  private JButton closeTabButton;
+
+  ClosableTab(String name, TabCloseCallBack closeCallBack) {
+    setName(name);
+    setLayout(null);
+
+    closeTabButton = new JButton("Close");
+    closeTabButton.setLocation(720, 5);
+    closeTabButton.setSize(new Dimension(70, 30));
+    closeTabButton.setFont(closeTabButton.getFont().deriveFont(10.0f));
+    add(closeTabButton);
+    closeTabButton.addActionListener(new AbstractAction() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        closeCallBack.call(name);
+      }
+    });
+  }
+
+  public interface TabCloseCallBack {
+    void call(String name);
+  }
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/FileSelectionBox.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/FileSelectionBox.java
new file mode 100644
index 0000000..5a6db5d
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/FileSelectionBox.java
@@ -0,0 +1,90 @@
+/*
+ * 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.iotdb.db.tools.logvisual.gui;
+
+import java.awt.event.ActionEvent;
+import java.io.File;
+import javax.swing.AbstractAction;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JTextField;
+
+public class FileSelectionBox extends Box{
+
+  private JLabel panelName;
+  private JTextField filePathField;
+  private JButton selectFileButton;
+  FilePathBoxSelectionCallBack callBack;
+
+  public FileSelectionBox(String name, FilePathBoxSelectionCallBack callBack, String
+      defaultFilePath) {
+    super(BoxLayout.X_AXIS);
+    this.callBack = callBack;
+
+    panelName = new JLabel(name);
+    filePathField = new JTextField("No file is selected");
+
+    filePathField.setEditable(false);
+    selectFileButton = new JButton("Select");
+    selectFileButton.addActionListener(new AbstractAction() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        onSelectFileButtonClick();
+      }
+    });
+
+    Box vBox = Box.createVerticalBox();
+    vBox.add(panelName);
+    vBox.add(filePathField);
+
+    add(vBox);
+    add(Box.createHorizontalStrut(10));
+    add(selectFileButton);
+
+    if (defaultFilePath != null) {
+      File defaultFile = new File(defaultFilePath);
+      if (!defaultFile.exists()) {
+        JOptionPane.showMessageDialog(this, panelName.getText() + ":default file " +
+            defaultFilePath + " does not exist");
+      } else {
+        filePathField.setText(defaultFilePath);
+        callBack.call(defaultFile);
+      }
+    }
+  }
+
+  private void onSelectFileButtonClick() {
+    JFileChooser fileChooser = new JFileChooser();
+    int status = fileChooser.showOpenDialog(this);
+    if (status == JFileChooser.APPROVE_OPTION) {
+      File chosenFile = fileChooser.getSelectedFile();
+      callBack.call(chosenFile);
+      filePathField.setText(chosenFile.getPath());
+    }
+  }
+
+  interface FilePathBoxSelectionCallBack {
+    void call(File chosenFile);
+  }
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/LabeledComboBox.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/LabeledComboBox.java
new file mode 100644
index 0000000..2852a6e
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/LabeledComboBox.java
@@ -0,0 +1,59 @@
+/*
+ * 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.iotdb.db.tools.logvisual.gui;
+
+import java.util.Map;
+import java.util.Vector;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.ComboBoxModel;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+
+public class LabeledComboBox<K, V> extends Box {
+
+  private ComboBoxModel comboBoxModel;
+  private JComboBox comboBox;
+
+  public LabeledComboBox(Map<K, V> itemMap, ComboSelectedCallback callback, String labelText) {
+    super(BoxLayout.Y_AXIS);
+
+    JLabel label = new JLabel(labelText);
+    add(label);
+
+    Vector vector = new Vector(itemMap.keySet());
+    vector.sort(null);
+    comboBoxModel = new DefaultComboBoxModel(vector);
+    comboBox = new JComboBox(comboBoxModel);
+    comboBox.setSelectedIndex(-1);
+
+    add(comboBox);
+
+    comboBox.addItemListener(e -> {
+      K key = (K) e.getItem();
+      callback.call(itemMap.get(key));
+    });
+  }
+
+  public interface ComboSelectedCallback {
+    void call(Object value);
+  }
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/LoadLogBox.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/LoadLogBox.java
new file mode 100644
index 0000000..e91191e
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/LoadLogBox.java
@@ -0,0 +1,61 @@
+/*
+ * 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.iotdb.db.tools.logvisual.gui;
+
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+import javax.swing.AbstractAction;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import org.apache.iotdb.db.tools.logvisual.LogVisualizer;
+
+public class LoadLogBox extends Box{
+
+  private JLabel status;
+  private JButton loadLogButton;
+
+  private LogVisualizer visualizer;
+
+  public LoadLogBox(LogVisualizer visualizer) {
+    super(BoxLayout.Y_AXIS);
+    this.visualizer = visualizer;
+
+    status = new JLabel("No logs are loaded");
+    loadLogButton = new JButton("Load logs");
+    loadLogButton.addActionListener(new AbstractAction() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        try {
+          visualizer.loadLogParser();
+          status.setText("Logs are successfully loaded");
+        } catch (IOException e1) {
+          JOptionPane.showMessageDialog(LoadLogBox.this, "Cannot load logs: " + e1);
+        }
+      }
+    });
+
+    add(status);
+    add(loadLogButton);
+  }
+
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/LogVisualizeGui.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/LogVisualizeGui.java
new file mode 100644
index 0000000..570d84c
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/LogVisualizeGui.java
@@ -0,0 +1,128 @@
+/*
+ * 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 ag [...]
+ */
+
+package org.apache.iotdb.db.tools.logvisual.gui;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import javax.swing.JFrame;
+import javax.swing.JOptionPane;
+import javax.swing.JTabbedPane;
+import javax.swing.WindowConstants;
+import org.apache.iotdb.db.tools.logvisual.LogVisualizer;
+import org.apache.iotdb.db.tools.logvisual.TimeSeriesStatistics;
+import org.jfree.chart.JFreeChart;
+
+public class LogVisualizeGui {
+
+  private final String DEFAULT_PROPERTY = "visual.config";
+  private final int DEFAULT_HEIGHT = 600;
+  private final int DEFAULT_WIDTH = 800;
+
+  private LogVisualizer visualizer;
+  private JFrame mainFrame;
+  private JTabbedPane tabbedPane;
+  private MainPanel mainPanel;
+
+  private Map<String, ResultPlotTab> resultPlotPanels = new HashMap<>();
+  private Map<String, ResultStatisticTab> resultTablePanels = new HashMap<>();
+  private String propertyFilePath;
+  private Properties properties;
+
+  public LogVisualizeGui(String propertyFilePath) throws IOException {
+    properties = new Properties();
+    if (propertyFilePath == null) {
+      propertyFilePath = DEFAULT_PROPERTY;
+    }
+    this.propertyFilePath = propertyFilePath;
+    File propertyFile = new File(propertyFilePath);
+    if (propertyFile.exists()) {
+      try (FileInputStream fileInputStream = new FileInputStream(propertyFilePath);
+      BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream)){
+        properties.load(bufferedInputStream);
+      }
+    }
+
+    this.visualizer = new LogVisualizer();
+    JFrame.setDefaultLookAndFeelDecorated(true);
+    mainFrame = new JFrame("Log Visualizer");
+    mainFrame.setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
+    mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+    mainFrame.setResizable(false);
+
+    tabbedPane = new JTabbedPane();
+    mainPanel = new MainPanel(visualizer, this::onPlanExecuted, properties, this::onPropertyChange);
+    tabbedPane.add(mainPanel, "Main");
+
+    mainFrame.add(tabbedPane);
+
+    mainFrame.setVisible(true);
+  }
+
+  private void onPlanExecuted(String planName, Map<String, JFreeChart> charts, Map<String,
+      List<TimeSeriesStatistics>> statisticMap) {
+    String tabName = planName + "-plot";
+    ResultPlotTab resultPlotTab = new ResultPlotTab(tabName, charts, this::onTabClose);
+    ResultPlotTab oldPlotTab = resultPlotPanels.get(tabName);
+    if (oldPlotTab != null) {
+      tabbedPane.remove(oldPlotTab);
+    }
+    resultPlotPanels.put(tabName, resultPlotTab);
+    tabbedPane.add(resultPlotTab);
+
+    tabName = planName + "-statistics";
+    ResultStatisticTab resultStatisticTab = new ResultStatisticTab(tabName, statisticMap,
+        this::onTabClose);
+    ResultStatisticTab oldTableTab = resultTablePanels.get(tabName);
+    if (oldTableTab != null) {
+      tabbedPane.remove(oldPlotTab);
+    }
+    resultTablePanels.put(tabName, resultStatisticTab);
+    tabbedPane.add(resultStatisticTab);
+  }
+
+  private void onPropertyChange(String key, String value) {
+    properties.put(key, value);
+    try (FileWriter writer = new FileWriter(propertyFilePath);
+        BufferedWriter bufferedWriter = new BufferedWriter(writer)) {
+      properties.store(bufferedWriter,"");
+    } catch (IOException e) {
+      JOptionPane.showMessageDialog(mainPanel, "Cannot save property files: ");
+    }
+  }
+
+  public interface PropertyChangeCallback {
+    void call(String key, String value);
+  }
+
+  private void onTabClose(String tabName) {
+    ClosableTab tab = resultTablePanels.remove(tabName);
+    if (tab != null) {
+      tabbedPane.remove(tab);
+      return;
+    }
+    tab = resultPlotPanels.remove(tabName);
+    if (tab != null) {
+      tabbedPane.remove(tab);
+    }
+  }
+
+  public static void main(String[] args) throws IOException {
+    String propertyFilePath = null;
+    if (args.length > 0) {
+      propertyFilePath = args[0];
+    }
+    LogVisualizeGui gui = new LogVisualizeGui(propertyFilePath);
+  }
+
+
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/MainPanel.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/MainPanel.java
new file mode 100644
index 0000000..3755c0a
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/MainPanel.java
@@ -0,0 +1,87 @@
+/*
+ * 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.iotdb.db.tools.logvisual.gui;
+
+import java.io.File;
+import java.util.Properties;
+import javax.swing.JPanel;
+import org.apache.iotdb.db.tools.logvisual.LogVisualizer;
+import org.apache.iotdb.db.tools.logvisual.VisualizationPlan;
+import org.apache.iotdb.db.tools.logvisual.conf.PropertyKeys;
+import org.apache.iotdb.db.tools.logvisual.gui.LogVisualizeGui.PropertyChangeCallback;
+import org.apache.iotdb.db.tools.logvisual.gui.PlanBox.ExecutePlanCallback;
+
+public class MainPanel extends JPanel {
+
+  private FileSelectionBox logFileSelectionBox;
+  private FileSelectionBox parserPropertyBox;
+  private LoadLogBox loadLogBox;
+  private PlanBox planBox;
+
+
+  private LogVisualizer visualizer;
+
+  private PropertyChangeCallback propertyChangeCallback;
+
+  public MainPanel(LogVisualizer logVisualizer, ExecutePlanCallback executePlanCallback,
+      Properties properties, PropertyChangeCallback propertyChangeCallback) {
+    this.visualizer = logVisualizer;
+    this.propertyChangeCallback = propertyChangeCallback;
+
+    setLayout(null);
+
+    logFileSelectionBox = new FileSelectionBox("LogFilePath", this::onLogFileSelected,
+        properties.getProperty(PropertyKeys.DEFAULT_LOG_FILE_PATH.getKey()));
+    logFileSelectionBox.setLocation(0, 0);
+    logFileSelectionBox.setSize(380, 40);
+
+    parserPropertyBox = new FileSelectionBox("ParserPropertyFilePath",
+        this::onParserPropertySelected, properties.getProperty(PropertyKeys
+        .DEFAULT_PARSER_FILE_PATH.getKey()));
+    parserPropertyBox.setLocation(0, 45);
+    parserPropertyBox.setSize(380, 40);
+
+    loadLogBox = new LoadLogBox(logVisualizer);
+    loadLogBox.setLocation(450, 0);
+    loadLogBox.setSize(300, 50);
+
+    planBox = new PlanBox(logVisualizer, executePlanCallback, properties.getProperty(PropertyKeys
+        .DEFAULT_PLAN_PATH.getKey()), propertyChangeCallback);
+    planBox.setLocation(0, 100);
+    planBox.setSize(750, 430);
+
+    add(logFileSelectionBox);
+    add(parserPropertyBox);
+    add(loadLogBox);
+    add(planBox);
+  }
+
+  private void onLogFileSelected(File logFile) {
+    visualizer.setLogFile(logFile);
+    propertyChangeCallback.call(PropertyKeys.DEFAULT_LOG_FILE_PATH.getKey(), logFile.getPath());
+  }
+
+  private void onParserPropertySelected(File parserPropertyFile) {
+    visualizer.setParserPropertyFile(parserPropertyFile);
+    propertyChangeCallback.call(PropertyKeys.DEFAULT_PARSER_FILE_PATH.getKey(), parserPropertyFile
+        .getPath());
+  }
+
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/PlanBox.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/PlanBox.java
new file mode 100644
index 0000000..a52399c
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/PlanBox.java
@@ -0,0 +1,189 @@
+/*
+ * 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.iotdb.db.tools.logvisual.gui;
+
+import java.awt.Dimension;
+import java.awt.ScrollPane;
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.DefaultListModel;
+import javax.swing.JButton;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ListSelectionModel;
+import javax.swing.event.ListSelectionEvent;
+import org.apache.iotdb.db.tools.logvisual.LogVisualizer;
+import org.apache.iotdb.db.tools.logvisual.TimeSeriesStatistics;
+import org.apache.iotdb.db.tools.logvisual.VisualizationPlan;
+import org.apache.iotdb.db.tools.logvisual.conf.PropertyKeys;
+import org.apache.iotdb.db.tools.logvisual.exceptions.AnalyzeException;
+import org.apache.iotdb.db.tools.logvisual.gui.LogVisualizeGui.PropertyChangeCallback;
+import org.jfree.chart.JFreeChart;
+
+public class PlanBox extends Box{
+  private JLabel panelName;
+  private JButton loadPlanButton;
+  private JButton executePlanButton;
+  private JButton savePlanButtion;
+  private JScrollPane scrollPane;
+  private DefaultListModel<VisualizationPlan> planListModel;
+  private JList planList;
+  private PlanDetailPanel planDetailPanel;
+
+  private LogVisualizer visualizer;
+
+  private ExecutePlanCallback executePlanCallback;
+  private PropertyChangeCallback propertyChangeCallback;
+
+  public PlanBox(LogVisualizer visualizer, ExecutePlanCallback executePlanCallback, String defaultPlanPath,
+      PropertyChangeCallback propertyChangeCallback) {
+    super(BoxLayout.X_AXIS);
+
+    this.visualizer = visualizer;
+    this.executePlanCallback = executePlanCallback;
+    this.propertyChangeCallback = propertyChangeCallback;
+
+    panelName = new JLabel("Visualization plans");
+    loadPlanButton = new JButton("Load plan");
+    executePlanButton = new JButton("Execute plan");
+    savePlanButtion = new JButton("Save plan");
+    panelName.setAlignmentX(CENTER_ALIGNMENT);
+    loadPlanButton.setAlignmentX(CENTER_ALIGNMENT);
+    executePlanButton.setAlignmentX(CENTER_ALIGNMENT);
+    savePlanButtion.setAlignmentX(CENTER_ALIGNMENT);
+
+    planListModel = new DefaultListModel<>();
+    planList = new JList<>(planListModel);
+    planList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+    scrollPane = new JScrollPane(planList);
+    add(scrollPane);
+    add(Box.createHorizontalStrut(10));
+
+    Box vBox = Box.createVerticalBox();
+    vBox.add(panelName);
+    vBox.add(loadPlanButton);
+    vBox.add(Box.createVerticalStrut(5));
+    vBox.add(executePlanButton);
+    vBox.add(Box.createVerticalStrut(5));
+    vBox.add(savePlanButtion);
+    vBox.add(Box.createGlue());
+    add(vBox);
+    setAlignmentY(0.5f);
+
+    planDetailPanel = new PlanDetailPanel();
+    planDetailPanel.setPreferredSize(new Dimension(400, 300));
+    add(planDetailPanel);
+
+    planList.addListSelectionListener(this::onPlanSelectionChanged);
+    loadPlanButton.addActionListener(this::onLoadPlanButtonClick);
+    executePlanButton.addActionListener(this::onExecutePlanButtonClick);
+    savePlanButtion.addActionListener(this::onPlanSave);
+
+    if (defaultPlanPath != null) {
+      String[] defaultPaths = defaultPlanPath.split(";");
+      File[] defaultPlanFiles = new File[defaultPaths.length];
+      for (int i = 0; i < defaultPaths.length; i++) {
+        defaultPlanFiles[i] = new File(defaultPaths[i]);
+      }
+      try {
+        visualizer.loadPlans(defaultPlanFiles);
+        Collection<VisualizationPlan> planList = visualizer.listPlans();
+        updatePlan(planList);
+      } catch (IOException e1) {
+        JOptionPane.showMessageDialog(this, "Cannot load plan: " + e1);
+      }
+    }
+  }
+
+  private void onLoadPlanButtonClick(ActionEvent e) {
+    JFileChooser fileChooser = new JFileChooser();
+    fileChooser.setMultiSelectionEnabled(true);
+    fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
+    int status = fileChooser.showOpenDialog(this);
+    if (status == JFileChooser.APPROVE_OPTION) {
+      File[] chosenFiles = fileChooser.getSelectedFiles();
+      try {
+        visualizer.loadPlans(chosenFiles);
+        Collection<VisualizationPlan> planList = visualizer.listPlans();
+        updatePlan(planList);
+
+        if (chosenFiles.length > 0) {
+          StringBuilder builder = new StringBuilder(chosenFiles[0].getPath());
+          for (int i = 1; i < chosenFiles.length; i++) {
+            builder.append(";").append(chosenFiles[i].getPath());
+          }
+          propertyChangeCallback.call(PropertyKeys.DEFAULT_PLAN_PATH.getKey(), builder.toString());
+        }
+      } catch (IOException e1) {
+        JOptionPane.showMessageDialog(this, "Cannot load plan: " + e1);
+      }
+    }
+  }
+
+  private void updatePlan(Collection<VisualizationPlan> plans) {
+    planListModel.clear();
+    for (VisualizationPlan plan : plans) {
+      planListModel.addElement(plan);
+    }
+  }
+
+  private void onExecutePlanButtonClick(ActionEvent e) {
+    VisualizationPlan plan = (VisualizationPlan) planList.getSelectedValue();
+    if (plan == null) {
+      return;
+    }
+    try {
+      visualizer.executePlan(plan);
+    } catch (AnalyzeException e1) {
+      JOptionPane.showMessageDialog(this, "Cannot execute plan: " + e1.getMessage());
+    }
+    Map<String, JFreeChart> charts = visualizer.getCharts();
+    Map<String, List<TimeSeriesStatistics>> statisticMap = visualizer.getStatisticsMap();
+    executePlanCallback.call(plan.getName(), charts, statisticMap);
+  }
+
+  private void onPlanSelectionChanged(ListSelectionEvent e) {
+    VisualizationPlan plan = (VisualizationPlan) planList.getSelectedValue();
+    if (plan == null) {
+      return;
+    }
+    planDetailPanel.setPlan(plan);
+  }
+
+  private void onPlanSave(ActionEvent e) {
+    planDetailPanel.updatePlan();
+  }
+
+  public interface ExecutePlanCallback {
+    void call(String planName, Map<String, JFreeChart> charts, Map<String,
+        List<TimeSeriesStatistics>> statisticMap);
+  }
+
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/PlanDetailPanel.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/PlanDetailPanel.java
new file mode 100644
index 0000000..7f0c80f
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/PlanDetailPanel.java
@@ -0,0 +1,216 @@
+/*
+ * 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.iotdb.db.tools.logvisual.gui;
+
+import java.text.SimpleDateFormat;
+import java.util.regex.Pattern;
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.JOptionPane;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+import org.apache.iotdb.db.tools.logvisual.LogEntry.LogLevel;
+import org.apache.iotdb.db.tools.logvisual.VisualUtils;
+import org.apache.iotdb.db.tools.logvisual.VisualizationPlan;
+
+public class PlanDetailPanel extends JScrollPane {
+
+  private VisualizationPlan plan;
+
+  private Box box;
+
+  private JTextField nameField = new JTextField();
+  private JTextField patternField = new JTextField();
+  private JTextField measurementsField = new JTextField();
+  private JTextField legendsField = new JTextField();
+  private JTextField tagField = new JTextField();
+  private JTextField levelField = new JTextField();
+  private JTextField threadNameField = new JTextField();
+  private JTextField classNameField = new JTextField();
+  private JTextField lineNumField = new JTextField();
+  private JTextField datePatternField = new JTextField();
+  private JTextField startDateField = new JTextField();
+  private JTextField endDateField = new JTextField();
+
+  public PlanDetailPanel() {
+    super(null, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+    setBorder(BorderFactory.createTitledBorder("Plan detail"));
+
+    nameField.setBorder(BorderFactory.createTitledBorder("Plan name"));
+    patternField.setBorder(BorderFactory.createTitledBorder("Content Pattern"));
+    measurementsField.setBorder(BorderFactory.createTitledBorder("Measurement positions"));
+    legendsField.setBorder(BorderFactory.createTitledBorder("Legends"));
+    tagField.setBorder(BorderFactory.createTitledBorder("Tag positions"));
+    levelField.setBorder(BorderFactory.createTitledBorder("Log level"));
+    threadNameField.setBorder(BorderFactory.createTitledBorder("Allowed thread names"));
+    classNameField.setBorder(BorderFactory.createTitledBorder("Allowed class names"));
+    lineNumField.setBorder(BorderFactory.createTitledBorder("Allowed line numbers"));
+    datePatternField.setBorder(BorderFactory.createTitledBorder("Date pattern"));
+    startDateField.setBorder(BorderFactory.createTitledBorder("Start date"));
+    endDateField.setBorder(BorderFactory.createTitledBorder("End date"));
+
+    Box box = Box.createVerticalBox();
+
+    box.add(nameField);
+    box.add(patternField);
+    box.add(measurementsField);
+    box.add(legendsField);
+    box.add(tagField);
+    box.add(levelField);
+    box.add(threadNameField);
+    box.add(classNameField);
+    box.add(lineNumField);
+    box.add(datePatternField);
+    box.add(startDateField);
+    box.add(endDateField);
+
+    setViewportView(box);
+  }
+
+  public void setPlan(VisualizationPlan plan) {
+    this.plan = plan;
+    updateFields();
+  }
+
+  private void updateFields() {
+    if (plan == null) {
+      return;
+    }
+    nameField.setText(plan.getName());
+    patternField.setText(plan.getContentPattern().pattern());
+    if (plan.getMeasurementPositions() != null) {
+      measurementsField.setText(VisualUtils.intArrayToString(plan.getMeasurementPositions()));
+    }
+    if (plan.getLegends() != null) {
+      legendsField.setText(String.join(",", plan.getLegends()));
+    }
+    if (plan.getTagPositions() != null) {
+      tagField.setText(VisualUtils.intArrayToString(plan.getTagPositions()));
+    }
+    levelField.setText(plan.getLogFilter().getMinLevel().name());
+    if (plan.getLogFilter().getThreadNameWhiteList() != null) {
+      threadNameField.setText(String.join(",", plan.getLogFilter().getThreadNameWhiteList()));
+    }
+    if (plan.getLogFilter().getClassNameWhiteList() != null) {
+      classNameField.setText(String.join(",", plan.getLogFilter().getClassNameWhiteList()));
+    }
+    if (plan.getLogFilter().getLineNumWhiteList() != null) {
+      lineNumField.setText(VisualUtils.intArrayToString(plan.getLogFilter().getLineNumWhiteList()));
+    }
+    if (plan.getLogFilter().getDatePatten() != null) {
+      SimpleDateFormat datePatten = (SimpleDateFormat) plan.getLogFilter().getDatePatten();
+      datePatternField.setText(datePatten.toPattern());
+      if (plan.getLogFilter().getStartDate() != null) {
+        startDateField.setText(datePatten.format(plan.getLogFilter().getStartDate()));
+      }
+      if (plan.getLogFilter().getEndDate() != null) {
+        endDateField.setText(datePatten.format(plan.getLogFilter().getEndDate()));
+      }
+    }
+  }
+
+  public void updatePlan() {
+    if (plan == null) {
+      return;
+    }
+
+    String name = nameField.getText();
+    String contentPattern = patternField.getText();
+    String measurementPositons = measurementsField.getText();
+    String legends = legendsField.getText();
+    String tagPositions = tagField.getText();
+    String logLevel = levelField.getText();
+    String allowedThreads = threadNameField.getText();
+    String allowedClasses = classNameField.getText();
+    String allowedLineNums = lineNumField.getText();
+    String datePattern = datePatternField.getText();
+    String startDate = startDateField.getText();
+    String endDate = endDateField.getText();
+
+    if (name.matches("\\s*")) {
+      JOptionPane.showMessageDialog(this, "Name cannot be empty");
+      return;
+    }
+    if (contentPattern.matches("\\s*")) {
+      JOptionPane.showMessageDialog(this, "Content pattern cannot be empty");
+      return;
+    }
+    if (measurementPositons.matches("\\s*")) {
+      measurementPositons = null;
+    }
+    if (legends.matches("\\s*")) {
+      legends = null;
+    }
+    if (tagPositions.matches("\\s*")) {
+      tagPositions = null;
+    }
+    if (logLevel.matches("\\s*")) {
+      logLevel = LogLevel.DEBUG.name();
+    }
+    if (allowedThreads.matches("\\s*")) {
+      allowedThreads = null;
+    }
+    if (allowedClasses.matches("\\s*")) {
+      allowedClasses = null;
+    }
+    if (allowedLineNums.matches("\\s*")) {
+      allowedLineNums = null;
+    }
+    if (datePattern.matches("\\s*")) {
+      datePattern = null;
+    }
+    if (startDate.matches("\\s*")) {
+      startDate = null;
+    }
+    if (endDate.matches("\\s*")) {
+      endDate = null;
+    }
+    if ((startDate != null || endDate != null) && datePattern == null) {
+      JOptionPane.showMessageDialog(this, "Date pattern cannot be empty if either start date or"
+          + " end date is not empty");
+      return;
+    }
+
+    try {
+      plan.setName(name);
+      plan.setContentPattern(Pattern.compile(contentPattern));
+      plan.setMeasurementPositions(measurementPositons != null ? VisualUtils.parseIntArray
+          (measurementPositons) : null);
+      plan.setLegends(legends != null ? legends.split(",") : null);
+      plan.setTagPositions(tagPositions != null ? VisualUtils.parseIntArray(tagPositions) : null);
+      plan.getLogFilter().setClassNameWhiteList(allowedClasses != null ? allowedClasses.split(",")
+          : null);
+      plan.getLogFilter().setThreadNameWhiteList(allowedThreads != null ? allowedThreads.split(","
+          + "") : null);
+      plan.getLogFilter().setLineNumWhiteList(allowedLineNums != null ? VisualUtils.parseIntArray
+          (allowedLineNums) : null);
+      plan.getLogFilter().setMinLevel(LogLevel.valueOf(logLevel));
+      SimpleDateFormat simpleDateFormat = datePattern != null ? new SimpleDateFormat(datePattern) :
+          null;
+      plan.getLogFilter().setDatePartten(simpleDateFormat);
+      plan.getLogFilter().setStartDate(startDate != null ? simpleDateFormat.parse(startDate) : null);
+      plan.getLogFilter().setEndDate(endDate != null ? simpleDateFormat.parse(endDate) : null);
+
+      plan.saveAsFile();
+    } catch (Exception e) {
+      JOptionPane.showMessageDialog(this, e.toString());
+    }
+  }
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/ResultPlotTab.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/ResultPlotTab.java
new file mode 100644
index 0000000..fa293e0
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/ResultPlotTab.java
@@ -0,0 +1,45 @@
+/*
+ * 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.iotdb.db.tools.logvisual.gui;
+
+import java.awt.GridBagLayout;
+import java.util.Map;
+import javax.swing.BorderFactory;
+import org.jfree.chart.ChartPanel;
+import org.jfree.chart.JFreeChart;
+
+class ResultPlotTab extends ClosableComboTab {
+
+  private ChartPanel chartPanel;
+
+  ResultPlotTab(String planName, Map<String, JFreeChart> charts, TabCloseCallBack closeCallBack) {
+    super(planName, charts, closeCallBack);
+
+    chartPanel = new ChartPanel(null);
+    chartPanel.setBorder(BorderFactory.createTitledBorder("Plot area"));
+    chartPanel.setLayout(new GridBagLayout());
+    chartPanel.setLocation(0, 60);
+    chartPanel.setSize(800, 480);
+    add(chartPanel);
+  }
+  void onItemSelected(Object chart){
+    chartPanel.setChart((JFreeChart) chart);
+  }
+}
\ No newline at end of file
diff --git a/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/ResultStatisticTab.java b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/ResultStatisticTab.java
new file mode 100644
index 0000000..f764c7c
--- /dev/null
+++ b/server/src/main/java/org/apache/iotdb/db/tools/logvisual/gui/ResultStatisticTab.java
@@ -0,0 +1,70 @@
+/*
+ * 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.iotdb.db.tools.logvisual.gui;
+
+import java.awt.BorderLayout;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.table.DefaultTableModel;
+import javax.swing.table.TableModel;
+import javax.swing.table.TableRowSorter;
+import org.apache.iotdb.db.tools.logvisual.TimeSeriesStatistics;
+
+class ResultStatisticTab extends ClosableTab {
+
+  private TableModel tableModel;
+  private JTable table;
+
+  ResultStatisticTab(String planName, Map<String, List<TimeSeriesStatistics>>
+      timeseriesStatistics, TabCloseCallBack closeCallBack) {
+    super(planName, closeCallBack);
+
+    table = new JTable();
+
+    Box box = Box.createVerticalBox();
+    box.add(table.getTableHeader());
+    box.add(table);
+    JScrollPane scrollPane = new JScrollPane(box);
+    scrollPane.setLocation(0, 100);
+    scrollPane.setSize(800, 600);
+    add(scrollPane);
+
+    Object[] header = TimeSeriesStatistics.HEADER;
+    List<TimeSeriesStatistics> allStatistics = new ArrayList<>();
+    for (List<TimeSeriesStatistics> seriesStatistics : timeseriesStatistics.values()) {
+      allStatistics.addAll(seriesStatistics);
+    }
+    allStatistics.sort(Comparator.comparing(TimeSeriesStatistics::getName));
+    Object[][] data = new Object[allStatistics.size()][];
+    for (int i = 0; i < allStatistics.size(); i++) {
+      data[i] = allStatistics.get(i).toArray();
+    }
+    tableModel = new DefaultTableModel(data, header);
+    table.setModel(tableModel);
+    table.setRowSorter(new TableRowSorter<>(tableModel));
+  }
+}
\ No newline at end of file