diff --git a/scouter.agent.host/src/scouter/agent/counter/task/RedisMonitor.java b/scouter.agent.host/src/scouter/agent/counter/task/RedisMonitor.java new file mode 100644 index 000000000..6575a16d6 --- /dev/null +++ b/scouter.agent.host/src/scouter/agent/counter/task/RedisMonitor.java @@ -0,0 +1,114 @@ +package scouter.agent.counter.task; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.HashSet; + +import scouter.agent.Configure; +import scouter.agent.counter.CounterBasket; +import scouter.agent.counter.anotation.Counter; +import scouter.lang.TimeTypeEnum; +import scouter.lang.pack.PerfCounterPack; +import scouter.lang.value.DecimalValue; +import scouter.lang.value.FloatValue; + +public class RedisMonitor { + private static HashSet floatSet = new HashSet(); + private static HashSet decimalSet = new HashSet(); + private Socket s; + + static { + floatSet.add("used_cpu_sys"); + floatSet.add("used_cpu_user"); + floatSet.add("used_cpu_sys_children"); + floatSet.add("used_cpu_user_children"); + floatSet.add("mem_fragmentation_ratio"); + decimalSet.add("uptime_in_seconds"); + decimalSet.add("uptime_in_days"); + decimalSet.add("lru_clock"); + decimalSet.add("connected_clients"); + decimalSet.add("connected_slaves"); + decimalSet.add("client_longest_output_list"); + decimalSet.add("client_biggest_input_buf"); + decimalSet.add("blocked_clients"); + decimalSet.add("used_memory"); + decimalSet.add("used_memory_rss"); + decimalSet.add("used_memory_peak"); + decimalSet.add("loading"); + decimalSet.add("aof_enabled"); + decimalSet.add("changes_since_last_save"); + decimalSet.add("bgsave_in_progress"); + decimalSet.add("bgrewriteaof_in_progress"); + decimalSet.add("total_connections_received"); + decimalSet.add("total_commands_processed"); + decimalSet.add("expired_keys"); + decimalSet.add("evicted_keys"); + decimalSet.add("keyspace_hits"); + decimalSet.add("keyspace_misses"); + decimalSet.add("pubsub_channels"); + decimalSet.add("pubsub_patterns"); + decimalSet.add("latest_fork_usec"); + decimalSet.add("vm_enabled"); + } + + @Counter(interval = 10000) + public void process(CounterBasket pw) throws IOException { + Configure conf = Configure.getInstance(); + + boolean redisEnabled = conf.getBoolean("redis_enabled", false); + + if (redisEnabled) { + String serverIp = conf.getValue("redis_server_ip", "127.0.0.1"); + int serverPort = conf.getInt("redis_server_port", 6379); + + String perfInfo = getRedisPerfInfo(serverIp, serverPort); + + String[] lines = perfInfo.split("\n"); + + PerfCounterPack p = pw.getPack(conf.getObjName(), TimeTypeEnum.REALTIME); + + for (String line : lines) { + String key = line.substring(0, line.indexOf(':')); + String value = line.substring(line.indexOf(':') + 1); + + if (floatSet.contains(key)) { + p.put(key, new FloatValue(Float.valueOf(value.trim()))); + } + + if (decimalSet.contains(key)) { + p.put(key, new DecimalValue(Long.valueOf(value.trim()))); + } + } + } + } + + private String getRedisPerfInfo(String serverIp, int serverPort) throws IOException { + s = new Socket(serverIp, serverPort); + InputStream is = s.getInputStream(); + OutputStream os = s.getOutputStream(); + os.write("INFO\r\n".getBytes()); + os.flush(); + + byte[] size = new byte[10]; + + int i = is.read(); + + int j = 0; + + while (i != '\n') { + size[j++] = (byte) i; + i = is.read(); + } + + int length = Integer.valueOf(new String(size, 1, j - 2)); + byte[] b = new byte[length]; + is.read(b); + + s.close(); + + return new String(b); + } + +} diff --git a/scouter.agent.java/src/scouter/agent/Configure.java b/scouter.agent.java/src/scouter/agent/Configure.java index 88dac2fe3..e41978d4f 100644 --- a/scouter.agent.java/src/scouter/agent/Configure.java +++ b/scouter.agent.java/src/scouter/agent/Configure.java @@ -31,8 +31,13 @@ public class Configure extends Thread { public static boolean JDBC_REDEFINED = false; private static Configure instance = null; + private long last_load_time = -1; + public Properties property = new Properties(); + private boolean running = true; + private File propertyFile; + long last_check = 0; - public final static synchronized Configure getInstance() { + public final static synchronized Configure getInstance() { if (instance == null) { instance = new Configure(); instance.setDaemon(true); @@ -220,6 +225,7 @@ public final static synchronized Configure getInstance() { //Experimental(ignoreset) public boolean __experimental = false; public boolean __control_connection_leak_autoclose_enabled = false; + public boolean __ip_dummy_test = false; //internal variables private int objHash; @@ -246,11 +252,10 @@ private Configure() { this.property = p; reload(false); } + private Configure(boolean b) { } - private long last_load_time = -1; - public Properties property = new Properties(); - private boolean running = true; + public void run() { Logger.println("Version " + Version.getAgentFullVersion()); long dateUnit = DateUtil.getDateUnit(); @@ -265,7 +270,6 @@ public void run() { ThreadUtil.sleep(3000); } } - private File propertyFile; public File getPropertyFile() { if (propertyFile != null) { return propertyFile; @@ -274,8 +278,7 @@ public File getPropertyFile() { propertyFile = new File(s.trim()); return propertyFile; } - long last_check = 0; - + public synchronized boolean reload(boolean force) { long now = System.currentTimeMillis(); if (force == false && now < last_check + 3000) @@ -470,7 +473,10 @@ private void apply() { //Experimental(ignoreset) this.__experimental = getBoolean("__experimental", false); - this.__control_connection_leak_autoclose_enabled = getBoolean("_control_connection_leak_autoclose_enabled", false); + this.__control_connection_leak_autoclose_enabled = getBoolean("__control_connection_leak_autoclose_enabled", false); + + //For testing + this.__ip_dummy_test = getBoolean("__ip_dummy_test", false); this.alert_perm_warning_pct = getInt("alert_perm_warning_pct", 90); this._hook_spring_rest_enabled = getBoolean("_hook_spring_rest_enabled", false); diff --git a/scouter.agent.java/src/scouter/agent/counter/task/JBossJMXPerf.java b/scouter.agent.java/src/scouter/agent/counter/task/JBossJMXPerf.java index 3e4584336..af60ce1eb 100644 --- a/scouter.agent.java/src/scouter/agent/counter/task/JBossJMXPerf.java +++ b/scouter.agent.java/src/scouter/agent/counter/task/JBossJMXPerf.java @@ -191,6 +191,8 @@ private void getContextList() { continue; } String statistics = mbean.getKeyProperty("statistics"); + + // JBOSS AS 7 if ("datasources".equals(subsystem) && "pool".equals(statistics)) { String name = mbean.getKeyProperty("data-source"); if (StringUtil.isNotEmpty(name)) { @@ -200,15 +202,14 @@ private void getContextList() { AgentHeartBeat.addObject(objType, HashUtil.hash(objName), objName); - add(objName, mbean, objType, ValueEnum.DECIMAL, "ActiveCount", + add(objName, mbean, objType, ValueEnum.DECIMAL, "InUseCount", CounterConstants.DATASOURCE_CONN_ACTIVE); add(objName, mbean, objType, ValueEnum.DECIMAL, "AvailableCount", - CounterConstants.DATASOURCE_CONN_IDLE); + CounterConstants.DATASOURCE_CONN_MAX); } catch (Exception e) { } } - } - if ("web".equals(subsystem)) { + } else if ("web".equals(subsystem)) { String connector = mbean.getKeyProperty("connector"); if (connector == null) { continue; diff --git a/scouter.agent.java/src/scouter/agent/counter/task/TomcatJMXPerf.java b/scouter.agent.java/src/scouter/agent/counter/task/TomcatJMXPerf.java index 8a963df75..5a447c360 100644 --- a/scouter.agent.java/src/scouter/agent/counter/task/TomcatJMXPerf.java +++ b/scouter.agent.java/src/scouter/agent/counter/task/TomcatJMXPerf.java @@ -190,6 +190,8 @@ private void getContextList() { CounterConstants.DATASOURCE_CONN_ACTIVE); add(objName, mbean, objType, ValueEnum.DECIMAL, "numIdle", CounterConstants.DATASOURCE_CONN_IDLE); + add(objName, mbean, objType, ValueEnum.DECIMAL, "maxActive", + CounterConstants.DATASOURCE_CONN_MAX); } catch (Exception e) { } } diff --git a/scouter.agent.java/src/scouter/agent/summary/ServiceSummary.java b/scouter.agent.java/src/scouter/agent/summary/ServiceSummary.java index 5969dd0f3..bac01b446 100644 --- a/scouter.agent.java/src/scouter/agent/summary/ServiceSummary.java +++ b/scouter.agent.java/src/scouter/agent/summary/ServiceSummary.java @@ -16,8 +16,6 @@ */ package scouter.agent.summary; -import java.util.Enumeration; - import scouter.agent.Configure; import scouter.agent.netio.data.DataProxy; import scouter.io.DataInputX; @@ -27,15 +25,13 @@ import scouter.lang.step.ApiCallStep; import scouter.lang.step.SqlStep; import scouter.lang.value.ListValue; -import scouter.util.BitUtil; -import scouter.util.IPUtil; -import scouter.util.IntIntLinkedMap; +import scouter.util.*; import scouter.util.IntIntLinkedMap.IntIntLinkedEntry; -import scouter.util.IntKeyLinkedMap; import scouter.util.IntKeyLinkedMap.IntKeyLinkedEntry; -import scouter.util.LongKeyLinkedMap; import scouter.util.LongKeyLinkedMap.LongKeyLinkedEntry; +import java.util.Enumeration; + public class ServiceSummary { private static ServiceSummary instance = null; @@ -73,24 +69,24 @@ public void process(XLogPack p) { } } - public ErrorData process(Throwable p, int message, int service, long txid, int sql, int api) { + public ErrorData process(Throwable thr, int message, int service, long txid, int sql, int api) { if (conf.summary_enabled == false) return null; - String errName = p.getClass().getName(); + String errName = thr.getClass().getName(); int errHash = DataProxy.sendError(errName); - ErrorData d = getSummaryError(errorMaster, BitUtil.composite(errHash, service)); - d.error = errHash; - d.service = service; - d.message = (message == 0 ? errHash : message); - d.count++; - d.txid = txid; + ErrorData errData = getSummaryError(errorMaster, BitUtil.composite(errHash, service)); + errData.error = errHash; + errData.service = service; + errData.message = (message == 0 ? errHash : message); + errData.count++; + errData.txid = txid; if (sql != 0) - d.sql = sql; + errData.sql = sql; if (api != 0) - d.apicall = api; - return d; + errData.apicall = api; + return errData; } public void process(SqlStep sqlStep) { diff --git a/scouter.agent.java/src/scouter/agent/trace/TraceMain.java b/scouter.agent.java/src/scouter/agent/trace/TraceMain.java index d160261e5..6aef03064 100644 --- a/scouter.agent.java/src/scouter/agent/trace/TraceMain.java +++ b/scouter.agent.java/src/scouter/agent/trace/TraceMain.java @@ -60,7 +60,7 @@ public Stat(TraceContext ctx) { private static IHttpTrace http = null; private static Configure conf = Configure.getInstance(); private static Error REJECT = new REQUEST_REJECT("service rejected"); - private static Error userTxNotClose = new USERTX_NOT_CLOSE("Missing Commit/Rollback Error"); + private static Error userTxNotClose = new USERTX_NOT_CLOSE("UserTransaction missing commit/rollback Error"); public static Object startHttpService(Object req, Object res) { try { @@ -293,7 +293,7 @@ public static void endHttpService(Object stat, Throwable thr) { ServiceSummary.getInstance().process(thr, pack.error, ctx.serviceHash, ctx.txid, 0, 0); } } else if (ctx.userTransaction > 0 && conf.xlog_error_check_user_transaction_enabled) { - pack.error = DataProxy.sendError("Missing Commit/Rollback Error"); + pack.error = DataProxy.sendError("UserTransaction missing commit/rollback Error"); ServiceSummary.getInstance().process(userTxNotClose, pack.error, ctx.serviceHash, ctx.txid, 0, 0); } if (ctx.group != null) { diff --git a/scouter.agent.java/src/scouter/xtra/http/HttpTrace.java b/scouter.agent.java/src/scouter/xtra/http/HttpTrace.java index 4ffc3b3de..6f124f18d 100644 --- a/scouter.agent.java/src/scouter/xtra/http/HttpTrace.java +++ b/scouter.agent.java/src/scouter/xtra/http/HttpTrace.java @@ -40,288 +40,306 @@ import java.util.Enumeration; public class HttpTrace implements IHttpTrace { - public HttpTrace() { - Configure conf = Configure.getInstance(); - this.remote_by_header = StringUtil.isEmpty(conf.trace_http_client_ip_header_key) == false; - this.http_remote_ip_header_key = conf.trace_http_client_ip_header_key; - - ConfObserver.add(HttpTrace.class.getName(), new Runnable() { - public void run() { - String x = Configure.getInstance().trace_http_client_ip_header_key; - if (CompareUtil.equals(x, http_remote_ip_header_key) == false) { - remote_by_header = StringUtil.isEmpty(x) == false; - http_remote_ip_header_key = x; - } - } - }); - } - - public String getParameter(Object req, String key) { - HttpServletRequest request = (HttpServletRequest) req; - - String ctype = request.getContentType(); - if (ctype != null && ctype.startsWith("application/x-www-form-urlencoded")) - return null; - - return request.getParameter(key); - } - - public String getHeader(Object req, String key) { - HttpServletRequest request = (HttpServletRequest) req; - return request.getHeader(key); - } - - public void start(TraceContext ctx, Object req, Object res) { - Configure conf = Configure.getInstance(); - HttpServletRequest request = (HttpServletRequest) req; - HttpServletResponse response = (HttpServletResponse) res; - - ctx.serviceName = getRequestURI(request); - ctx.serviceHash = HashUtil.hash(ctx.serviceName); - - if(ctx.serviceHash == conf.getEndUserPerfEndpointHash()){ - ctx.isStaticContents=true; - processEndUserData(request); - return; - } - - ctx.isStaticContents = TraceMain.isStaticContents(ctx.serviceName); - - - ctx.http_method = request.getMethod(); - ctx.http_query = request.getQueryString(); - ctx.http_content_type = request.getContentType(); - - ctx.remoteIp = getRemoteAddr(request); - - try { - switch (conf.trace_user_mode) { - case 2: - ctx.userid = UseridUtil.getUserid(request, response); - break; - case 1: - ctx.userid = UseridUtil.getUseridCustom(request, response, conf.trace_user_session_key); - if (ctx.userid == 0 && ctx.remoteIp != null) { - ctx.userid = HashUtil.hash(ctx.remoteIp); - } - break; - default: - if (ctx.remoteIp != null) { - ctx.userid = HashUtil.hash(ctx.remoteIp); - } - break; - } - MeterUsers.add(ctx.userid); - } catch (Throwable e) { - // ignore - } - String referer = request.getHeader("Referer"); - if (referer != null) { - ctx.referer = DataProxy.sendReferer(referer); - } - String userAgent = request.getHeader("User-Agent"); - if (userAgent != null) { - ctx.userAgent = DataProxy.sendUserAgent(userAgent); - ctx.userAgentString = userAgent; - } - dump(ctx.profile, request, ctx); - if (conf.trace_interservice_enabled) { - try { - String gxid = request.getHeader(conf._trace_interservice_gxid_header_key); - if (gxid != null) { - ctx.gxid = Hexa32.toLong32(gxid); - } - String txid = request.getHeader(conf._trace_interservice_callee_header_key); - if (txid != null) { - ctx.txid = Hexa32.toLong32(txid); - ctx.is_child_tx = true; - } - String caller = request.getHeader(conf._trace_interservice_caller_header_key); - if (caller != null) { - ctx.caller = Hexa32.toLong32(caller); - ctx.is_child_tx = true; - } - } catch (Throwable t) { - } - } - - if (conf.trace_response_gxid_enabled && !ctx.isStaticContents) { - try { - if (ctx.gxid == 0) - ctx.gxid = ctx.txid; - - String resGxId = Hexa32.toString32(ctx.gxid) + ":" + ctx.startTime; - response.setHeader(conf._trace_interservice_gxid_header_key, resGxId); - - Cookie c = new Cookie(conf._trace_interservice_gxid_header_key, resGxId); - response.addCookie(c); - - } catch (Throwable t) { - } - } - - if (conf.trace_webserver_enabled) { - try { - ctx.web_name = request.getHeader(conf.trace_webserver_name_header_key); - String web_time = request.getHeader(conf.trace_webserver_time_header_key); - if (web_time != null) { - int x = web_time.indexOf("t="); - if (x >= 0) { - web_time = web_time.substring(x + 2); - x = web_time.indexOf(' '); - if (x > 0) { - web_time = web_time.substring(0, x); - } - ctx.web_time = (int) (System.currentTimeMillis() - (Long.parseLong(web_time) / 1000)); - } - } - } catch (Throwable t) { - } - } - } - - private void processEndUserData(HttpServletRequest request) { - EndUserNavigationData nav; - EndUserErrorData err; - EndUserAjaxData ajax; - - if("err".equals(request.getParameter("p"))) { - EndUserErrorData data = new EndUserErrorData(); + private boolean remote_by_header; + private boolean __ip_dummy_test; + private String http_remote_ip_header_key; + + public static String[] ipRandom = {"27.114.0.121", "58.3.128.121", + "101.53.64.121", "125.7.128.121", "202.68.224.121", "62.241.64.121", "86.63.224.121", "78.110.176.121", + "84.18.128.121", "95.142.176.121", "61.47.128.121", "110.76.32.121", "116.251.64.121", "123.150.0.121", + "125.254.128.121", "5.134.32.0", "5.134.32.121", "52.119.0.121", "154.0.128.121", "190.46.0.121"}; + + public HttpTrace() { + Configure conf = Configure.getInstance(); + this.remote_by_header = !StringUtil.isEmpty(conf.trace_http_client_ip_header_key); + this.__ip_dummy_test = conf.__ip_dummy_test; + + ConfObserver.add(HttpTrace.class.getName(), new Runnable() { + public void run() { + String x = Configure.getInstance().trace_http_client_ip_header_key; + if (CompareUtil.equals(x, http_remote_ip_header_key) == false) { + remote_by_header = StringUtil.isEmpty(x) == false; + http_remote_ip_header_key = x; + } + } + }); + } + + public String getParameter(Object req, String key) { + HttpServletRequest request = (HttpServletRequest) req; + + String ctype = request.getContentType(); + if (ctype != null && ctype.startsWith("application/x-www-form-urlencoded")) + return null; + + return request.getParameter(key); + } + + public String getHeader(Object req, String key) { + HttpServletRequest request = (HttpServletRequest) req; + return request.getHeader(key); + } + + public void start(TraceContext ctx, Object req, Object res) { + Configure conf = Configure.getInstance(); + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + + ctx.serviceName = getRequestURI(request); + ctx.serviceHash = HashUtil.hash(ctx.serviceName); + + if (ctx.serviceHash == conf.getEndUserPerfEndpointHash()) { + ctx.isStaticContents = true; + processEndUserData(request); + return; + } + + ctx.isStaticContents = TraceMain.isStaticContents(ctx.serviceName); + + + ctx.http_method = request.getMethod(); + ctx.http_query = request.getQueryString(); + ctx.http_content_type = request.getContentType(); + + ctx.remoteIp = getRemoteAddr(request); + + try { + switch (conf.trace_user_mode) { + case 2: + ctx.userid = UseridUtil.getUserid(request, response); + break; + case 1: + ctx.userid = UseridUtil.getUseridCustom(request, response, conf.trace_user_session_key); + if (ctx.userid == 0 && ctx.remoteIp != null) { + ctx.userid = HashUtil.hash(ctx.remoteIp); + } + break; + default: + if (ctx.remoteIp != null) { + ctx.userid = HashUtil.hash(ctx.remoteIp); + } + break; + } + MeterUsers.add(ctx.userid); + } catch (Throwable e) { + // ignore + } + String referer = request.getHeader("Referer"); + if (referer != null) { + ctx.referer = DataProxy.sendReferer(referer); + } + String userAgent = request.getHeader("User-Agent"); + if (userAgent != null) { + ctx.userAgent = DataProxy.sendUserAgent(userAgent); + ctx.userAgentString = userAgent; + } + dump(ctx.profile, request, ctx); + if (conf.trace_interservice_enabled) { + try { + String gxid = request.getHeader(conf._trace_interservice_gxid_header_key); + if (gxid != null) { + ctx.gxid = Hexa32.toLong32(gxid); + } + String txid = request.getHeader(conf._trace_interservice_callee_header_key); + if (txid != null) { + ctx.txid = Hexa32.toLong32(txid); + ctx.is_child_tx = true; + } + String caller = request.getHeader(conf._trace_interservice_caller_header_key); + if (caller != null) { + ctx.caller = Hexa32.toLong32(caller); + ctx.is_child_tx = true; + } + } catch (Throwable t) { + } + } + + if (conf.trace_response_gxid_enabled && !ctx.isStaticContents) { + try { + if (ctx.gxid == 0) + ctx.gxid = ctx.txid; + + String resGxId = Hexa32.toString32(ctx.gxid) + ":" + ctx.startTime; + response.setHeader(conf._trace_interservice_gxid_header_key, resGxId); + + Cookie c = new Cookie(conf._trace_interservice_gxid_header_key, resGxId); + response.addCookie(c); + + } catch (Throwable t) { + } + } + + if (conf.trace_webserver_enabled) { + try { + ctx.web_name = request.getHeader(conf.trace_webserver_name_header_key); + String web_time = request.getHeader(conf.trace_webserver_time_header_key); + if (web_time != null) { + int x = web_time.indexOf("t="); + if (x >= 0) { + web_time = web_time.substring(x + 2); + x = web_time.indexOf(' '); + if (x > 0) { + web_time = web_time.substring(0, x); + } + ctx.web_time = (int) (System.currentTimeMillis() - (Long.parseLong(web_time) / 1000)); + } + } + } catch (Throwable t) { + } + } + } + + private void processEndUserData(HttpServletRequest request) { + EndUserNavigationData nav; + EndUserErrorData err; + EndUserAjaxData ajax; + + if ("err".equals(request.getParameter("p"))) { + EndUserErrorData data = new EndUserErrorData(); data.count = 1; - data.stacktrace = DataProxy.sendError(StringUtil.nullToEmpty(request.getParameter("stacktrace"))); - data.userAgent = DataProxy.sendUserAgent(StringUtil.nullToEmpty(request.getParameter("userAgent"))); + data.stacktrace = DataProxy.sendError(StringUtil.nullToEmpty(request.getParameter("stacktrace"))); + data.userAgent = DataProxy.sendUserAgent(StringUtil.nullToEmpty(request.getParameter("userAgent"))); data.host = DataProxy.sendServiceName(StringUtil.nullToEmpty(request.getParameter("host"))); data.uri = DataProxy.sendServiceName(StringUtil.nullToEmpty(request.getParameter("uri"))); - data.message = DataProxy.sendError(StringUtil.nullToEmpty(request.getParameter("message"))); + data.message = DataProxy.sendError(StringUtil.nullToEmpty(request.getParameter("message"))); data.name = DataProxy.sendError(StringUtil.nullToEmpty(request.getParameter("name"))); data.file = DataProxy.sendServiceName(StringUtil.nullToEmpty(request.getParameter("file"))); - data.lineNumber = CastUtil.cint(request.getParameter("lineNumber")); - data.columnNumber = CastUtil.cint(request.getParameter("columnNumber")); + data.lineNumber = CastUtil.cint(request.getParameter("lineNumber")); + data.columnNumber = CastUtil.cint(request.getParameter("columnNumber")); //Logger.println("@ input error data -> print"); //Logger.println(data); EndUserSummary.getInstance().process(data); - } else if("nav".equals(request.getParameter("p"))) { - - } else if("ax".equals(request.getParameter("p"))) { - - } - - //EndUserSummary.getInstance().process(p); - - } - - private String getRequestURI(HttpServletRequest request) { - String uri = request.getRequestURI(); - if (uri == null) - return "no-url"; - int x = uri.indexOf(';'); - if (x > 0) - return uri.substring(0, x); - else - return uri; - } - - private boolean remote_by_header; - private String http_remote_ip_header_key; - - private String getRemoteAddr(HttpServletRequest request) { - try { - if (remote_by_header) { - return request.getHeader(http_remote_ip_header_key); - } else { - return request.getRemoteAddr(); - } - } catch (Throwable t) { - remote_by_header = false; - return "0.0.0.0"; - } - } - - public void end(TraceContext ctx, Object req, Object res) { - // HttpServletRequest request = (HttpServletRequest)req; - // HttpServletResponse response = (HttpServletResponse)res; - // - } - - private static void dump(IProfileCollector p, HttpServletRequest request, TraceContext ctx) { - Configure conf = Configure.getInstance(); - if (conf.profile_http_querystring_enabled) { - String msg = request.getMethod() + " ?" + StringUtil.trimToEmpty(request.getQueryString()); - MessageStep step = new MessageStep(msg); - step.start_time = (int) (System.currentTimeMillis() - ctx.startTime); - p.add(step); - } - if (conf.profile_http_header_enabled) { - if (conf.profile_http_header_url_prefix == null || ctx.serviceName.indexOf(conf.profile_http_header_url_prefix) >= 0) { - Enumeration en = request.getHeaderNames(); - if (en != null) { - int start_time = (int) (System.currentTimeMillis() - ctx.startTime); - while (en.hasMoreElements()) { - String key = (String) en.nextElement(); - String value = new StringBuilder().append("header: ").append(key).append("=") - .append(StringUtil.limiting(request.getHeader(key), 1024)).toString(); - - MessageStep step = new MessageStep(value); - step.start_time = start_time; - // step.start_cpu = (int) (SysJMX.getCurrentThreadCPU() - // - - // ctx.startCpu); - - p.add(step); - } - } - } - } - if (conf.profile_http_parameter_enabled) { - if (conf.profile_http_parameter_url_prefix == null || ctx.serviceName.indexOf(conf.profile_http_parameter_url_prefix) >= 0) { - String ctype = request.getContentType(); - if (ctype != null && ctype.indexOf("multipart") >= 0) - return; - - Enumeration en = request.getParameterNames(); - if (en != null) { - int start_time = (int) (System.currentTimeMillis() - ctx.startTime); - while (en.hasMoreElements()) { - String key = (String) en.nextElement(); - String value = new StringBuilder().append("parameter: ").append(key).append("=") - .append(StringUtil.limiting(request.getParameter(key), 1024)).toString(); - - MessageStep step = new MessageStep(value); - step.start_time = start_time; - // step.start_cpu = (int) (SysJMX.getCurrentThreadCPU() - // - ctx.startCpu); - - p.add(step); - } - } - } - } - } - - public void rejectText(Object res, String text) { - HttpServletResponse response = (HttpServletResponse) res; - try { - PrintWriter pw = response.getWriter(); - pw.println(text); - } catch (IOException e) { - } - } - - public void rejectUrl(Object res, String url) { - HttpServletResponse response = (HttpServletResponse) res; - try { - response.sendRedirect(url); - } catch (IOException e) { - } - } - - - public static void main(String[] args) { - System.out.println("http trace".indexOf(null)); - } + } else if ("nav".equals(request.getParameter("p"))) { + + } else if ("ax".equals(request.getParameter("p"))) { + + } + + //EndUserSummary.getInstance().process(p); + + } + + private String getRequestURI(HttpServletRequest request) { + String uri = request.getRequestURI(); + if (uri == null) + return "no-url"; + int x = uri.indexOf(';'); + if (x > 0) + return uri.substring(0, x); + else + return uri; + } + + private String getRemoteAddr(HttpServletRequest request) { + try { + //For Testing + if (__ip_dummy_test) { + return getRandomIp(); + } + + if (remote_by_header) { + return request.getHeader(http_remote_ip_header_key); + } else { + return request.getRemoteAddr(); + } + + } catch (Throwable t) { + remote_by_header = false; + return "0.0.0.0"; + } + } + + private String getRandomIp() { + int len = ipRandom.length; + int randomNum = (int) (Math.random() * (len-1)); + return ipRandom[randomNum]; + } + + public void end(TraceContext ctx, Object req, Object res) { + // HttpServletRequest request = (HttpServletRequest)req; + // HttpServletResponse response = (HttpServletResponse)res; + // + } + + private static void dump(IProfileCollector p, HttpServletRequest request, TraceContext ctx) { + Configure conf = Configure.getInstance(); + if (conf.profile_http_querystring_enabled) { + String msg = request.getMethod() + " ?" + StringUtil.trimToEmpty(request.getQueryString()); + MessageStep step = new MessageStep(msg); + step.start_time = (int) (System.currentTimeMillis() - ctx.startTime); + p.add(step); + } + if (conf.profile_http_header_enabled) { + if (conf.profile_http_header_url_prefix == null || ctx.serviceName.indexOf(conf.profile_http_header_url_prefix) >= 0) { + Enumeration en = request.getHeaderNames(); + if (en != null) { + int start_time = (int) (System.currentTimeMillis() - ctx.startTime); + while (en.hasMoreElements()) { + String key = (String) en.nextElement(); + String value = new StringBuilder().append("header: ").append(key).append("=") + .append(StringUtil.limiting(request.getHeader(key), 1024)).toString(); + + MessageStep step = new MessageStep(value); + step.start_time = start_time; + // step.start_cpu = (int) (SysJMX.getCurrentThreadCPU() + // - + // ctx.startCpu); + + p.add(step); + } + } + } + } + if (conf.profile_http_parameter_enabled) { + if (conf.profile_http_parameter_url_prefix == null || ctx.serviceName.indexOf(conf.profile_http_parameter_url_prefix) >= 0) { + String ctype = request.getContentType(); + if (ctype != null && ctype.indexOf("multipart") >= 0) + return; + + Enumeration en = request.getParameterNames(); + if (en != null) { + int start_time = (int) (System.currentTimeMillis() - ctx.startTime); + while (en.hasMoreElements()) { + String key = (String) en.nextElement(); + String value = new StringBuilder().append("parameter: ").append(key).append("=") + .append(StringUtil.limiting(request.getParameter(key), 1024)).toString(); + + MessageStep step = new MessageStep(value); + step.start_time = start_time; + // step.start_cpu = (int) (SysJMX.getCurrentThreadCPU() + // - ctx.startCpu); + + p.add(step); + } + } + } + } + } + + public void rejectText(Object res, String text) { + HttpServletResponse response = (HttpServletResponse) res; + try { + PrintWriter pw = response.getWriter(); + pw.println(text); + } catch (IOException e) { + } + } + + public void rejectUrl(Object res, String url) { + HttpServletResponse response = (HttpServletResponse) res; + try { + response.sendRedirect(url); + } catch (IOException e) { + } + } + + + public static void main(String[] args) { + System.out.println("http trace".indexOf(null)); + } } \ No newline at end of file diff --git a/scouter.client/src/scouter/client/net/DummyTcpProxy.java b/scouter.client/src/scouter/client/net/DummyTcpProxy.java index 308f9d5de..9339011a0 100644 --- a/scouter.client/src/scouter/client/net/DummyTcpProxy.java +++ b/scouter.client/src/scouter/client/net/DummyTcpProxy.java @@ -21,6 +21,7 @@ import java.util.List; import scouter.lang.pack.Pack; +import scouter.lang.value.Value; public class DummyTcpProxy extends TcpProxy { @@ -32,4 +33,16 @@ public List process(String cmd, Pack param) { public synchronized void process(String cmd, Object param, INetReader recv) { } + + public Pack getSingle(String cmd, Pack param) { + return null; + } + + public Value getSingleValue(String cmd, Pack param) { + return null; + } + + public List processValues(String cmd, Pack param) { + return new ArrayList(0); + } } diff --git a/scouter.client/src/scouter/client/net/TcpProxy.java b/scouter.client/src/scouter/client/net/TcpProxy.java index f6427ba44..21b842d38 100644 --- a/scouter.client/src/scouter/client/net/TcpProxy.java +++ b/scouter.client/src/scouter/client/net/TcpProxy.java @@ -44,7 +44,7 @@ protected TcpProxy(int serverId) { public static synchronized TcpProxy getTcpProxy(int serverId) { Server server = ServerManager.getInstance().getServer(serverId); - if (server == null || server.isOpen() == false) { + if (server == null || server.isOpen() == false || server.isConnected() == false) { return new DummyTcpProxy(); } ConnectionPool pool = server.getConnectionPool(); diff --git a/scouter.client/src/scouter/client/server/ServerManager.java b/scouter.client/src/scouter/client/server/ServerManager.java index a9a0799c9..6beb7d55e 100644 --- a/scouter.client/src/scouter/client/server/ServerManager.java +++ b/scouter.client/src/scouter/client/server/ServerManager.java @@ -78,14 +78,16 @@ private void syncServerTime() { TcpProxy tcp = TcpProxy.getTcpProxy(server.getId()); try { MapPack p = (MapPack) tcp.getSingle(RequestCmd.SERVER_STATUS, null); - long time = p.getLong("time"); - if (time > 0) { - server.setDelta(time); + if (p != null) { + long time = p.getLong("time"); + if (time > 0) { + server.setDelta(time); + } + long usedMemory = p.getLong("used"); + long totalMemory = p.getLong("total"); + server.setUsedMemory(usedMemory); + server.setTotalMemory(totalMemory); } - long usedMemory = p.getLong("used"); - long totalMemory = p.getLong("total"); - server.setUsedMemory(usedMemory); - server.setTotalMemory(totalMemory); } catch (Throwable th) { th.printStackTrace(); } finally { diff --git a/scouter.client/src/scouter/client/threads/SessionObserver.java b/scouter.client/src/scouter/client/threads/SessionObserver.java index a45a5d512..478d97aa4 100644 --- a/scouter.client/src/scouter/client/threads/SessionObserver.java +++ b/scouter.client/src/scouter/client/threads/SessionObserver.java @@ -52,7 +52,7 @@ public void run() { if (server.isConnected() == false && server.getConnectionPool().size() < 1) { server.setSession(0); // reset session } - if (server.isConnected() && server.getSession() == 0) { + if (/*server.isConnected() &&*/ server.getSession() == 0) { boolean success = LoginMgr.silentLogin(server, server.getUserId(), server.getPassword()); if (success) { ConsoleProxy.infoSafe("Success re-login to " + server.getName()); diff --git a/scouter.common/src/scouter/lang/counters/CounterConstants.java b/scouter.common/src/scouter/lang/counters/CounterConstants.java index 176d3f373..2bd3ef617 100644 --- a/scouter.common/src/scouter/lang/counters/CounterConstants.java +++ b/scouter.common/src/scouter/lang/counters/CounterConstants.java @@ -81,6 +81,7 @@ public class CounterConstants { public final static String DATASOURCE_CONN_ACTIVE = "ConnActive"; public final static String DATASOURCE_CONN_IDLE = "ConnIdle"; + public final static String DATASOURCE_CONN_MAX = "ConnMax"; public final static String HOST_CPU = "Cpu"; public final static String HOST_SYSCPU = "SysCpu"; diff --git a/scouter.common/src/scouter/lang/counters/counters.xml b/scouter.common/src/scouter/lang/counters/counters.xml index dfcff5b2e..220e7a72d 100644 --- a/scouter.common/src/scouter/lang/counters/counters.xml +++ b/scouter.common/src/scouter/lang/counters/counters.xml @@ -11,20 +11,20 @@ mrhit : MariaDB HitRatio - + - + - + - + @@ -68,6 +68,7 @@ mrhit : MariaDB HitRatio + diff --git a/scouter.common/src/scouter/net/RequestCmd.java b/scouter.common/src/scouter/net/RequestCmd.java index b83c0b00c..07fd545b7 100644 --- a/scouter.common/src/scouter/net/RequestCmd.java +++ b/scouter.common/src/scouter/net/RequestCmd.java @@ -242,6 +242,8 @@ public class RequestCmd { public static final String VISITOR_REALTIME_TOTAL = "VISITOR_REALTIME_TOTAL"; public static final String VISITOR_LOADDATE = "VISITOR_LOADDATE"; public static final String VISITOR_LOADDATE_TOTAL = "VISITOR_LOADDATE_TOTAL"; + public static final String VISITOR_LOADDATE_GROUP = "VISITOR_LOADDATE_GROUP"; + public static final String VISITOR_LOADHOUR_GROUP = "VISITOR_LOADHOUR_GROUP"; // SUMMARY public static final String LOAD_SERVICE_SUMMARY = "LOAD_SERVICE_SUMMARY"; diff --git a/scouter.common/src/scouter/util/FileUtil.java b/scouter.common/src/scouter/util/FileUtil.java index 3d8c601c8..2301d8044 100644 --- a/scouter.common/src/scouter/util/FileUtil.java +++ b/scouter.common/src/scouter/util/FileUtil.java @@ -1,54 +1,42 @@ -/* - * Copyright 2015 the original author or authors. - * @https://github.com/scouter-project/scouter - * - * Licensed 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. - */ - +/* + * Copyright 2015 the original author or authors. + * @https://github.com/scouter-project/scouter + * + * Licensed 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 scouter.util; -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.RandomAccessFile; -import java.io.Reader; -import java.io.Writer; -import java.net.DatagramSocket; -import java.net.ServerSocket; -import java.net.Socket; -import java.nio.channels.FileChannel; -import java.util.Properties; - -import scouter.io.DataInputX; -import scouter.io.DataOutputX; +import scouter.io.DataInputX; +import scouter.io.DataOutputX; + +import java.io.*; +import java.net.DatagramSocket; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.channels.FileChannel; +import java.util.Properties; public class FileUtil { - - public static DatagramSocket close(DatagramSocket udp){ - try { - if (udp != null) { - udp.close(); - } - } catch (Throwable e) { - } - return null; + + public static DatagramSocket close(DatagramSocket udp){ + try { + if (udp != null) { + udp.close(); + } + } catch (Throwable e) { + } + return null; } public static InputStream close(InputStream in) { try { @@ -266,18 +254,18 @@ public static String getJarLocation(Class class1) { } catch (Exception e) { } return null; - } - public static String getJarFileName(Class class1) { - try { - String path = "" + class1.getResource("/" + class1.getName().replace('.', '/') + ".class"); - if (path.indexOf("!") < 0) - return null; - path = path.substring("jar:file:".length(), path.indexOf("!")); - return new File(path).getAbsolutePath(); - } catch (Exception e) { - } - return null; - } + } + public static String getJarFileName(Class class1) { + try { + String path = "" + class1.getResource("/" + class1.getName().replace('.', '/') + ".class"); + if (path.indexOf("!") < 0) + return null; + path = path.substring("jar:file:".length(), path.indexOf("!")); + return new File(path).getAbsolutePath(); + } catch (Exception e) { + } + return null; + } public static void main(String[] args) throws IOException { String path = getJarLocation(FileUtil.class); System.out.println(path); diff --git a/scouter.deploy/build-server.xml b/scouter.deploy/build-server.xml index 2afdbe12c..c1d8eaebd 100644 --- a/scouter.deploy/build-server.xml +++ b/scouter.deploy/build-server.xml @@ -35,7 +35,7 @@ - + diff --git a/scouter.document/client/How-To-Use-Client.md b/scouter.document/client/How-To-Use-Client.md index 68890af1b..275da3a05 100644 --- a/scouter.document/client/How-To-Use-Client.md +++ b/scouter.document/client/How-To-Use-Client.md @@ -3,21 +3,19 @@ ![Screen](../img/client/client-view.png) -* **(1)** Perspective를 관리한다. 이미 만들어진 화면 구성이 있다면 (+)를 클릭하여 추가할 수 있다. -* **(2)** 현재의 Perspective(화면구성)이다. 화면에서 새로운 차트를 추가하고 재배치했다면 (2)에서 오른쪽 마우스를 눌러 다른 이름으로 저장할 수 있다. 이전 원래 구성(Service)은 (1)을 눌러서 다시 추가할 수있다. -* **(3)** 오픈잭트 익스플로서의 툴바 버튼들이다. 오브젝트를 필터링하거나 인액티브 상태의 오브젝트들을화면에서 제거하는 메뉴들이 있다. -* **(4)** 액티브 서비스 이퀄라이져이다. 바를 더블 클릭하면 현재의 액티브서비스 리스트를 상세히 조회 할 수 있다. -* **(5)** XLOG 차트의 응답시간 축이다. (5) 글자 부근을 클릭하면 명시적으로 값을 입력할 수 있다. 또는 화면을 한번 선택한 후에 up/down키을 이용하여 응답시간 축의 MAX값을 변경할 수있다. -* **(6)** 현재 XLOG 차트에 나타나고 있는 트랜잭션 수이다. -* **(7)** 오브젝트 네비게이션 창이다 화면에서 최상의(sjhost_6100)오브젝트는 Scouter서버이다. - * 서버의 상태 조회나 하위 객체들을 전체 그룹관점에서 조회할 수 있는 메뉴들이 링크되어있다. - * 메뉴는 오른쪽 마우스를 통해 선택할 수 있다. 오른쪽 마우스를 클릭하면 시스템에 대한 성능들을 조회할 수 있다. - * Scouter서버 아래의 sjhost는 서버OS를 의미한다.그리고 tomcat1/tomcat2는 JAVAEE 타입의 오브잭트이다. JAVA/WAS과련 성능 정보를 조회할 수 있으며 화면의 XLOG또한 Tomcat타입이 디플트이다. - * 임의의 오브젝트를 **Doublic click**하면 선택한 오브젝트로 필터링 된다 -* **(8)** 클라이언트의 힙메모리 사용량이다. 이것을 보고 메모리 부족을 판단할 수 있다. -* **(9)** XLOG을 제어하기 위한 툴바, 에러만보기, 혹은 필터링을 위한 화면을 열수 있다. -* **(10)** 오브잭트별 대표 성응조표를 조회 할 수 있다. 예를 들어 host type(sjhost)은 cpu - tomcat type(tomcat1)은 Active Service Count가 보여진다. -* **(11)** XLOG의 X 축은 트랜잭션이 종료된 시간이다. (<- ->)이등이나 (+/-) 확대축소가 가능하다. -* **(12)** XLOG에서는 왼쪽 마우스를 드레그 하여 영역을 선택할 수 있고 해당 영역의 점들(트랜잭션)의 - 성능을 상세 분석할 수 있다. \ No newline at end of file +* **(1)** Manage perspectives. Any existed perspectives can be added by clicking (+) button. +* **(2)** It is currently viewed perspectives. Perspective you cutomized like adding new chart can be save to new name, by clicking save button on floating menu. You can load previous perspective as well along to (1). +* **(3)** This is toolbar buttons on object explorer. You can mange objectes currenly loaded to show monitoring metrics. Adding filter to objects, removing objects which is not used. +* **(4)** Active service equalizer. When you double-click the bar, you can see more detailed active service list. +* **(5)** Y-axis of response time on XLOG chart. The maximum(highest on Y-axis) value can be modified by clicking axis and input new specific value. Or after activating XLOG chart, use up/down arrow key. +* **(6)** The number of transactions displayed on XLOG chart. +* **(7)** Object navigation view. Top most object(sjhost_6100) is Scouter server. + * With a view of monitoring group, you can monitor each server's status and its services on tree view. + * You can see each monitring metrics by clicking right mouse button on each object. + * 'sjhost' below Scounter server (sjhost_6100) means watching server host. And tomcat1 and 2 is JAVAEE object. The XLOG chart on example screen is Tomcat mode, and it is default view of it. + * You can filter the object type by **double-clicking** you want. +* **(8)** The HEAP memory usage on client program. You can check memory shortage. +* **(9)** Toolbar for managing displayed data on XLOG chart. It is supported filtering diaglog, only error transactions, and so on. +* **(10)** It is the realtime monitoring value. For example, host type object is showing CPU usage, Tomcat type is displaying active service counter. +* **(11)** X-axis on XLOG chart is used to show the end time of each transaction. To move time use left/right arrow key. To zoom in/out use + or - key. +* **(12)** To see more detailed transaction data, drag markers on XLOG chart. diff --git a/scouter.document/client/How-To-Use-Client_kr.md b/scouter.document/client/How-To-Use-Client_kr.md index b21c5a9bb..620a87354 100644 --- a/scouter.document/client/How-To-Use-Client_kr.md +++ b/scouter.document/client/How-To-Use-Client_kr.md @@ -4,20 +4,18 @@ ![Screen](../img/client/client-view.png) * **(1)** Perspective를 관리한다. 이미 만들어진 화면 구성이 있다면 (+)를 클릭하여 추가할 수 있다. -* **(2)** 현재의 Perspective(화면구성)이다. 화면에서 새로운 차트를 추가하고 재배치했다면 (2)에서 오른쪽 마우스를 눌러 다른 이름으로 저장할 수 있다. 이전 원래 구성(Service)은 (1)을 눌러서 다시 추가할 수있다. -* **(3)** 오픈잭트 익스플로서의 툴바 버튼들이다. 오브젝트를 필터링하거나 인액티브 상태의 오브젝트들을화면에서 제거하는 메뉴들이 있다. -* **(4)** 액티브 서비스 이퀄라이져이다. 바를 더블 클릭하면 현재의 액티브서비스 리스트를 상세히 조회 할 수 있다. -* **(5)** XLOG 차트의 응답시간 축이다. (5) 글자 부근을 클릭하면 명시적으로 값을 입력할 수 있다. 또는 화면을 한번 선택한 후에 up/down키을 이용하여 응답시간 축의 MAX값을 변경할 수있다. -* **(6)** 현재 XLOG 차트에 나타나고 있는 트랜잭션 수이다. -* **(7)** 오브젝트 네비게이션 창이다 화면에서 최상의(sjhost_6100)오브젝트는 Scouter서버이다. - * 서버의 상태 조회나 하위 객체들을 전체 그룹관점에서 조회할 수 있는 메뉴들이 링크되어있다. - * 메뉴는 오른쪽 마우스를 통해 선택할 수 있다. 오른쪽 마우스를 클릭하면 시스템에 대한 성능들을 조회할 수 있다. - * Scouter서버 아래의 sjhost는 서버OS를 의미한다.그리고 tomcat1/tomcat2는 JAVAEE 타입의 오브잭트이다. JAVA/WAS과련 성능 정보를 조회할 수 있으며 화면의 XLOG또한 Tomcat타입이 디플트이다. - * 임의의 오브젝트를 **Doublic click**하면 선택한 오브젝트로 필터링 된다 -* **(8)** 클라이언트의 힙메모리 사용량이다. 이것을 보고 메모리 부족을 판단할 수 있다. -* **(9)** XLOG을 제어하기 위한 툴바, 에러만보기, 혹은 필터링을 위한 화면을 열수 있다. -* **(10)** 오브잭트별 대표 성응조표를 조회 할 수 있다. 예를 들어 host type(sjhost)은 cpu - tomcat type(tomcat1)은 Active Service Count가 보여진다. -* **(11)** XLOG의 X 축은 트랜잭션이 종료된 시간이다. (<- ->)이등이나 (+/-) 확대축소가 가능하다. -* **(12)** XLOG에서는 왼쪽 마우스를 드레그 하여 영역을 선택할 수 있고 해당 영역의 점들(트랜잭션)의 - 성능을 상세 분석할 수 있다. \ No newline at end of file +* **(2)** 현재 로드된 Perspective(화면구성)이다. 현재 Perspective 에 새로운 차트를 추가하고 재배치했다면 (2)에서 오른쪽 마우스를 눌러 다른 이름으로 저장할 수 있다. 원래 보던 Perspective 는 (1) 을 눌러서 다시 로드할 수있다. +* **(3)** 오픈젝트 익스플로러의 툴바 버튼들이다. 오브젝트를 필터링하거나 인액티브 상태의 오브젝트들을 화면에서 제거하는 메뉴들이 있다. +* **(4)** 액티브 서비스 이퀄라이져이다. 바를 더블 클릭하면 현재 액티브 서비스 리스트를 상세히 조회 할 수 있다. +* **(5)** XLog 차트의 응답시간 축이다. MAX 시간 값을 변경하려면 (5) 글자 부근을 클릭하여 값을 명시적으로 입력한다. 그리고 화면을 한 번 선택한 후 Up / Down 키을 이용하여도 된다. +* **(6)** 현재 XLog 차트에 나타나고 있는 트랜잭션 수이다. +* **(7)** 오브젝트 네비게이션 View 이다. 화면에서 최상의(sjhost_6100)오브젝트는 Scouter 서버이다. + * 모니터링 그룹 관점에서 그룹 내 하위 서버의 상태나 오브젝트들을 조회할 수 있는 메뉴들이 링크되어 있다. + * 메뉴는 오른쪽 마우스를 통해 선택할 수 있다. 오른쪽 마우스를 클릭하면 시스템에 대한 성능들을 조회할 수 있다. + * Scouter 서버 아래의 sjhost는 서버 Host 를 의미한다. 그리고 tomcat1/tomcat2는 JAVA EE 타입의 오브젝트이다. JAVA 나 WAS 관련 성능 정보를 조회할 수 있으며 화면의 XLog 또한 Tomcat 타입이 디플트이다. + * 임의의 오브젝트를 **Doublic click**하면 선택한 오브젝트로 필터링 된다. +* **(8)** Scouter Client 의 힙메모리 사용량이다. 이것을 보고 클라이언트 프로그램의 메모리 부족을 판단할 수 있다. +* **(9)** XLog 를 제어하기 위한 툴바, 에러 보기, 혹은 상세 필터링을 위한 화면을 열수 있다. +* **(10)** 오브젝트별 대표 성능지표를 조회 할 수 있다. 예를 들어 host 타입(sjhost)은 CPU 가, Tomcat 타입(tomcat1)은 Active Service Count 가 출력된다. +* **(11)** XLog 의 X 축은 트랜잭션이 종료된 시간이다. 좌/우 방향키로 이동할 수 있고, +/- 키로 확대 축소가 가능하다. +* **(12)** XLog 에서 왼쪽 마우스를 드레그하여 영역을 선택할 수 있고, 영역에 속한 점들(트랜잭션)의 성능을 상세 분석할 수 있다. diff --git a/scouter.document/client/Reading-XLog.md b/scouter.document/client/Reading-XLog.md index 068b1a4ed..dc2df5372 100644 --- a/scouter.document/client/Reading-XLog.md +++ b/scouter.document/client/Reading-XLog.md @@ -1,17 +1,13 @@ -# 응답시간 분포도(XLog) 보는 방법 +# How to read XLog chart ![Englsh](https://img.shields.io/badge/language-English-red.svg) [![Korean](https://img.shields.io/badge/language-Korean-blue.svg)](Reading-XLog_kr.md) -하나의 트랜잭션(서비스 수행)을 하나의 점으로 표현하는 차트이다. -세로축은 응답시간으로 가로축은 종료시간으로 출력한다. -실시간 화면은 일정시간(2초) 간격으로 좌측으로 쉬프트 된다. +XLOG chart is for realtime transaction monitoring. Each marker represents individual transaction. X-axis is for the end time of transaction, and Y-axis is for the response time of it. The chart is reloaded every 2 seconds, pushing old things to left. ![XLog](../img/client/xlog.png) -Scouter에서는 응답 분포 차트를 XLog라고 부른다. XLog는 Transaction Log라는 의미에서 2004년에 처음 만들어졌다. -XLog는 전체 트랜잭션을 한눈에 파악할 수 있고 느린 트랜잭션만을 선별하여 조회할 수있기 때문에 응용 프로그램을 튜닝하는데 가장 효과적이 모니터링 방법이라고 할 수있다. +XLog chart is a kind of point chart, response-time-ditribution chart. XLog was developed in 2004. XLog is summarising current status of system with a perspective of reponse time. XLOG charting and anlaysis is very effective to tune application's performance, while sorting high response time. ![TxList](../img/client/xlog_txlist.png) ![Profile](../img/client/xlog_profile.png) -위 화면에서 처럼 왼쪽마우스 버튼을 이용하여 드레그하면 일부 점들을 선별하여 선택할 수 있다. 이렇게 선택된 트랜잭션들은 화면처럼 리스트로 나타난다. -리스트에서 하나를 선택하면 해당 트랜잭션에 대한 상세 정보를 볼 수 있다. +As you can see the screenshot, it is able to crop mouse dragging area. You can see each marker's detailed transaction information on the dragged result list. diff --git a/scouter.document/client/Reading-XLog_kr.md b/scouter.document/client/Reading-XLog_kr.md index 136ea26f5..347fe6322 100644 --- a/scouter.document/client/Reading-XLog_kr.md +++ b/scouter.document/client/Reading-XLog_kr.md @@ -1,17 +1,16 @@ -# 응답시간 분포도(XLog) 보는 방법 +# XLog 보는 방법 [![Englsh](https://img.shields.io/badge/language-English-red.svg)](Reading-XLog.md) ![Korean](https://img.shields.io/badge/language-Korean-blue.svg) -하나의 트랜잭션(서비스 수행)을 하나의 점으로 표현하는 차트이다. -세로축은 응답시간으로 가로축은 종료시간으로 출력한다. -실시간 화면은 일정시간(2초) 간격으로 좌측으로 쉬프트 된다. - ![XLog](../img/client/xlog.png) -Scouter에서는 응답 분포 차트를 XLog라고 부른다. XLog는 Transaction Log라는 의미에서 2004년에 처음 만들어졌다. -XLog는 전체 트랜잭션을 한눈에 파악할 수 있고 느린 트랜잭션만을 선별하여 조회할 수있기 때문에 응용 프로그램을 튜닝하는데 가장 효과적이 모니터링 방법이라고 할 수있다. +Scouter 에서 응답시간 분포 그래프를 XLog 라고 부른다. XLog 는 Transaction Log 라는 의미에서 2004년에 처음 만들어졌다. +XLog 는 전체 트랜잭션을 한눈에 파악할 수 있고 느린 트랜잭션을 선별하여 조회할 수 있기 때문에 응용 프로그램을 튜닝하는데 가장 효과적인 방법이라 할 수 있다. + +XLog 차트는 하나의 트랜잭션(서비스 수행)을 하나의 점으로 표현하는 차트이다. +세로축은 트랜잭션이 수행된 응답시간, 가로축은 트랜잭션의 종료시간이다. +화면의 그래프는 일정시간(2초) 간격으로 Reload 된다. 이때 그래프는 좌측으로 쉬프트 된다. ![TxList](../img/client/xlog_txlist.png) ![Profile](../img/client/xlog_profile.png) -위 화면에서 처럼 왼쪽마우스 버튼을 이용하여 드레그하면 일부 점들을 선별하여 선택할 수 있다. 이렇게 선택된 트랜잭션들은 화면처럼 리스트로 나타난다. -리스트에서 하나를 선택하면 해당 트랜잭션에 대한 상세 정보를 볼 수 있다. +위 화면에서처럼 왼쪽마우스 버튼을 이용하여 드레그하면 일부 점들을 선별하여 선택할 수 있다. 이렇게 선택된 트랜잭션들은 화면처럼 리스트로 나타난다. 리스트에서 하나를 선택하면 해당 트랜잭션에 대한 상세 정보를 볼 수 있다. diff --git a/scouter.document/main/Live-Demo_kr.md b/scouter.document/main/Live-Demo_kr.md index 4db7ad3f7..10d31a596 100644 --- a/scouter.document/main/Live-Demo_kr.md +++ b/scouter.document/main/Live-Demo_kr.md @@ -1,10 +1,10 @@ # Quick Start [![Englsh](https://img.shields.io/badge/language-English-red.svg)](Live-Demo.md) ![Korean](https://img.shields.io/badge/language-Korean-blue.svg) -Scouter를 바로 사용해 볼 수 있도록 Live 데모 시스템이 구성되어 있으며 -클라이언트만 다운로드 하여 접속해 볼 수 있다 +Scouter 를 바로 사용해 볼 수 있도록 Live 데모 시스템이 구성되어 있으며 +클라이언트만 다운로드 하여 접속해 볼 수 있다. -현재 Live 데모 시스템은 v0.4.6으로 구성되어 있으므로 v0.4.6 클라이언트로 접속하여 볼 수 있다 +현재 Live 데모 시스템은 v0.4.6 으로 구성되어 있으므로 v0.4.6 클라이언트로 접속하여 볼 수 있다 1. [Client(Viewer) 다운로드](https://github.com/scouter-project/scouter/releases/tag/v0.4.6) 2. Client 실행하고 아래 정보를 입력하여 Scouter 데모 서버로 접속한다. diff --git a/scouter.document/main/Setup-FAQ.md b/scouter.document/main/Setup-FAQ.md index ec79bdb88..188afadd7 100644 --- a/scouter.document/main/Setup-FAQ.md +++ b/scouter.document/main/Setup-FAQ.md @@ -1,81 +1,69 @@ # Setup FAQ ![Englsh](https://img.shields.io/badge/language-English-red.svg) [![Korean](https://img.shields.io/badge/language-Korean-blue.svg)](Setup-FAQ_kr.md) -설치과정에서 발생하는 빈번한 질문에 대해서 정리한다. +This is FAQ about installation step. -## Scouter 구성은 어떻게 되는가? -Scouter는 에이전트, 서버, 클라이언트 구조를 가지고 있다. -에이전트는 데이터를 수집하고 서버에 전송한다. -서버는 데이터를 재 가공하고 저장하며, 클라이언트의 요청에 응답한다. -클라이언트는 서버로 부터 성능데이터를 조회하여 화면에 차트를 포함한 다양한 방법으로 보여준다. +## How about Scouter's architecture? +Scouter has agent-server-client architecture. Scouter Agent collects monitoring data and sends it to Scouter Server. Scouter Server processes and accumulates received data and sends it to Scouter Client by response result. Scouter Client reads processed monitoring and performance data from Server, display them on the user's screen with various views and perspectives. ``` Agent[Linux] ----\ Agent[Tomcat] ------> Server ——-> Client Agent[MariaDB] ----/ ``` -Agent는 다시 Linux용 Agent와 Tomcat용 그리고 MariaDB용 에이전트로 구분할 수 있다. 앞으로 더많은 오픈소스가 포함될 예정이다. +Currently there are Tomcat, Linux, and MariaDB agents. In the future more agents for more opensource software would be developed and included. -## Tomcat과 Scouter Server 그리고 클라이언트를 분리하여 서로다른 서버에서 수행하려면 -Scouter의 에이전트 서버 클라이언트는 TCP/UDP를 통해서 통신한다 따라서 적절한 설정을 통해 -상호 연결관계를 설정할 수 있다. +## How to install Scouter Server, Agent, Client each to different machines? +Scouter's three SW use TCP and UDP for their communication method. With configuring IP addresses and ports along to your environment, you can install three SW independently. -### 에이전트가 서버로 전송할때 -UDP와 TCP 두가지 프로토코을 통해 데이터를 전송한다. -에이전트가 수집하는 일반적인 성능정보는 모두 UDP를 통해 전송한다. -TCP는 서버의 요청을 받아서 데이터를 전송한다. +### How to communicate between Agent and Server +Scouter Agent uses both TCP and UDP. Agent reports its data via UDP to Scouter Server periodically. And uses TCP when reply Server's special requests. ``` UDP 6100 Agent ====> Server TCP 6100 ``` -따라서 에이전트에서는 서버에 대한 IP주소는 127.0.0.1을 기본으로 한다. 이것을 바꾸려면 +Agent is configured to use 127.0.0.1 for Server's IP address. To change this, modify java options on Agent program. -Agent가 설치된 Tomcat의 java 옵션에서 다음과 같이 설정할 수 있다. ``` -Dsever_addr=192.168.0.1 -Dserver_udp_port=6100 -Dserver_tcp_port=6100 ``` -또는 scouter설정 파일에 설정할 수도 있다. 설정파일은 -Dscouter.config=scouter.conf 형식으로 설정한다. +Also configurable to modify configuration properties file. ``` sever_addr=192.168.0.1 server_udp_port=6100 server_tcp_port=6100 ``` +If you want to another configuration file, uses -Dscouter.config= option. -### 클라이언트가 서버에 요청할때 -클라이언트는 서버에 데이터를 요청할때 TCP6100을 사용한다. 에이전트가 접속할때와 동일한 포트를 사용한다. +### How to communicate between Server and Client +Scouter Client uses TCP 6100 port to request monitoring data, same as Agent does. ``` Clinet ——-> Server TCP 6100 ``` -서버는 6100TCP를 통해 에이전트와 클라이언트 모두와 통신한다. - -클라이언트는 처음 실행하여 로그인할때 서버에 대한 연결정보를 입력하도록 되어있다. +TCP 6100 port are used by both Agent and Client. User should specify Server's endpoint information when Client program run at first. Server Address: 192.168.0.1:6100 -## 여러 Tomcat 인스턴스를 모니터링 하려면 설정해야하는 것은? -Scouter는 모니터링 대상에게 계층형 이름을 부여하여 논리적으로 관리한다. +## How to monitor multiple Tomcat instances +Scouter used logical names and hierarchy for its monitoring targets. /host/tomcat/context -형식이다. 그런데 한 서버에 여러 톰켓인스턴스를 설치하는 경우에는 Scouter서버가 바라보는 모니터링 오브젝트의 이름이 중복된다. 이런경우 각 인스턴스를 위한 이름을 부여해야한다. - -Agent가 설치된 Tomcat의 java 옵션에서 다음과 같이 설정할 수 있다. +If you want to watch multiple Tomcat instances, you should give unique name of each on the same hierachy. +Names can be given by java options like, ``` -Dscouter_name=MyTomcatInstName ``` -혹은 scouter.config에 - +or on scouter.config file, ``` scouter_name=MyTomcatInstName ``` -라고 설정할 수 있다. 위 방법중에 한가지만 사용하면 된다. -그러면 클라이언트 화면에서 여러개의 톰켓 인스턴스를 확인할 수있다. -## Tomcat이 설치된 서버의 Cpu, Mem모니터링 하고 싶을때 -scouter.agent.host를 릴리드할 예정임 +## How to monitor Tomcat host CPU and Memory simutaneously +TBD diff --git a/scouter.document/main/Setup-FAQ_kr.md b/scouter.document/main/Setup-FAQ_kr.md index 1a42f8483..a9c684a3f 100644 --- a/scouter.document/main/Setup-FAQ_kr.md +++ b/scouter.document/main/Setup-FAQ_kr.md @@ -4,23 +4,22 @@ 설치과정에서 발생하는 빈번한 질문에 대해서 정리한다. ## Scouter 구성은 어떻게 되는가? -Scouter는 에이전트, 서버, 클라이언트 구조를 가지고 있다. +Scouter 는 에이전트, 서버, 클라이언트 구조를 가지고 있다. 에이전트는 데이터를 수집하고 서버에 전송한다. -서버는 데이터를 재 가공하고 저장하며, 클라이언트의 요청에 응답한다. +서버는 데이터를 재가공하고 저장하며, 클라이언트의 요청에 응답한다. 클라이언트는 서버로 부터 성능데이터를 조회하여 화면에 차트를 포함한 다양한 방법으로 보여준다. ``` Agent[Linux] ----\ Agent[Tomcat] ------> Server ——-> Client Agent[MariaDB] ----/ ``` -Agent는 다시 Linux용 Agent와 Tomcat용 그리고 MariaDB용 에이전트로 구분할 수 있다. 앞으로 더많은 오픈소스가 포함될 예정이다. +에이전트는 Linux용, Tomcat 용, 그리고 MariaDB 용 에이전트로 구분할 수 있다. 앞으로 더 많은 오픈소스가 포함될 예정이다. -## Tomcat과 Scouter Server 그리고 클라이언트를 분리하여 서로다른 서버에서 수행하려면 -Scouter의 에이전트 서버 클라이언트는 TCP/UDP를 통해서 통신한다 따라서 적절한 설정을 통해 -상호 연결관계를 설정할 수 있다. +## Tomcat, Scouter 서버, 클라이언트를 서로 다른 서버에서 수행하려면 +Scouter 의 에이전트, 서버, 클라이언트는 TCP/UDP 소켓으로 통신한다. 따라서 적절한 설정을 통해 상호 연결관계를 설정할 수 있다. -### 에이전트가 서버로 전송할때 -UDP와 TCP 두가지 프로토코을 통해 데이터를 전송한다. +### 에이전트가 서버로 전송할 때 +UDP 와 TCP 두 가지 프로토콜을 통해 데이터를 전송한다. 에이전트가 수집하는 일반적인 성능정보는 모두 UDP를 통해 전송한다. TCP는 서버의 요청을 받아서 데이터를 전송한다. ``` @@ -28,15 +27,14 @@ TCP는 서버의 요청을 받아서 데이터를 전송한다. Agent ====> Server TCP 6100 ``` -따라서 에이전트에서는 서버에 대한 IP주소는 127.0.0.1을 기본으로 한다. 이것을 바꾸려면 - -Agent가 설치된 Tomcat의 java 옵션에서 다음과 같이 설정할 수 있다. +에이전트 프로그램의 Scouter 서버에 대한 IP 주소는 127.0.0.1 가 디폴트 이다. 이것을 바꾸려면 +Agent가 설치된 Tomcat의 java 옵션에 다음과 같이 수정한다. ``` -Dsever_addr=192.168.0.1 -Dserver_udp_port=6100 -Dserver_tcp_port=6100 ``` -또는 scouter설정 파일에 설정할 수도 있다. 설정파일은 -Dscouter.config=scouter.conf 형식으로 설정한다. +또는 Scouter 설정파일에 추가 할 수도 있다. 설정파일의 path 정보는 -Dscouter.config=scouter.conf 형식으로 설정한다. ``` sever_addr=192.168.0.1 server_udp_port=6100 @@ -45,23 +43,22 @@ server_tcp_port=6100 ### 클라이언트가 서버에 요청할때 -클라이언트는 서버에 데이터를 요청할때 TCP6100을 사용한다. 에이전트가 접속할때와 동일한 포트를 사용한다. +클라이언트는 서버에 데이터를 요청할때 TCP 6100 포트를 사용한다. 에이전트가 접속할 때와 동일한 포트를 사용한다. ``` Clinet ——-> Server TCP 6100 ``` -서버는 6100TCP를 통해 에이전트와 클라이언트 모두와 통신한다. - -클라이언트는 처음 실행하여 로그인할때 서버에 대한 연결정보를 입력하도록 되어있다. +서버는 TCP 6100 포트를 통해 에이전트와 클라이언트 모두와 통신한다. +클라이언트는 처음 실행될 때 Scouter 서버에 대한 연결정보를 입력하도록 되어있다. Server Address: 192.168.0.1:6100 -## 여러 Tomcat 인스턴스를 모니터링 하려면 설정해야하는 것은? +## 여러 Tomcat 인스턴스를 모니터링 하려면 설정해야 하는 것은? Scouter는 모니터링 대상에게 계층형 이름을 부여하여 논리적으로 관리한다. /host/tomcat/context -형식이다. 그런데 한 서버에 여러 톰켓인스턴스를 설치하는 경우에는 Scouter서버가 바라보는 모니터링 오브젝트의 이름이 중복된다. 이런경우 각 인스턴스를 위한 이름을 부여해야한다. +형식이다. 한 서버에 여러 톰켓 인스턴스를 설치하는 경우에는 Scouter 서버가 바라보는 모니터링 오브젝트의 이름이 중복된다. 그렇기 때문에 각 인스턴스에 고유한 이름을 부여해야 한다. Agent가 설치된 Tomcat의 java 옵션에서 다음과 같이 설정할 수 있다. ``` @@ -72,10 +69,10 @@ Agent가 설치된 Tomcat의 java 옵션에서 다음과 같이 설정할 수 ``` scouter_name=MyTomcatInstName ``` -라고 설정할 수 있다. 위 방법중에 한가지만 사용하면 된다. -그러면 클라이언트 화면에서 여러개의 톰켓 인스턴스를 확인할 수있다. +라고 설정할 수 있다. 위 방법 중에 한가지를 사용하면 된다. +그러면 클라이언트 화면에서 여러 개의 톰켓 인스턴스를 확인할 수있다. -## Tomcat이 설치된 서버의 Cpu, Mem모니터링 하고 싶을때 +## Tomcat이 설치된 서버의 CPU, Memory 를 모니터링 하고 싶을때 scouter.agent.host를 릴리드할 예정임 diff --git a/scouter.document/main/What-special-in-SCOUTER_kr.md b/scouter.document/main/What-special-in-SCOUTER_kr.md index 6b7c8a8d4..3a34d866a 100644 --- a/scouter.document/main/What-special-in-SCOUTER_kr.md +++ b/scouter.document/main/What-special-in-SCOUTER_kr.md @@ -1,30 +1,28 @@ # What special in SOUTER [![Englsh](https://img.shields.io/badge/language-English-red.svg)](What-special-in-SCOUTER.md) ![Korean](https://img.shields.io/badge/language-Korean-blue.svg) -SCOUTER는 무엇인가? -SCOUTER는 애플리케이션의 성능을 모니터링하고 분석할 수 있다. -SCOUTER는 그만의 특징들이 있다. +Scouter 는 무엇인가? +Scouter 는 애플리케이션의 성능을 모니터링하고 분석할 수 있다. +Scouter 는 그만의 특징들이 있다. -## 설치형 솔루션이다. -SCOUTER를 사용하기 위해서는 모니터링 대상 시스템과 같은 내부망에 수집서버를 설치해야한다. -그렇게 함으로 SCOUTER는 좀더 많은 데이터를 대상시스템으로 부터 수집한다. -이점은 SaaS형 모니터링 솔루션과 크게 다른점이다. +## 설치형 솔루션 +Scouter 를 사용하기 위해서 모니터링 대상 시스템과 같은 내부망에 수집 서버를 설치해야 한다. +이렇게 구성할 경우 SaaS 형 모니터링 솔루션에 비해 보다 상세한 데이터를 수집할 수 있다. +SaaS 형은 설치가 필요없기에 쉽게 사용할 수 있는 반면, Scouter 와 같은 설치형은 수집된 데이터가 보다 상세하기 때문에 분석이 효율적이다. -SaaS형은 쉽게 사용할 수 있는 반면 설치형은 보다 상세한 분석이 가능하다. +## 클라이언트를 뷰어 +Scouter Client 는 Eclipse RCP Platform 으로 만들어지 독립 클라이언트이다. 그래서 웹형 뷰어보다 많은 성능 데이터를 제어할 수 있다. -## SCOUTER는 독립클라이언트를 뷰어로 사용한다.(WEB 기반이 아니) -SCOUTER는 Eclipse RCP platform으로 만들어지 독립 클라이언트이다. 그래서 웹형 뷰어보다 많은 성능 데이터를 제어할 수 있다. +## Scouter 파일 DB에 성능 데이터를 저장 -## SCOUTER 파일 DB에 성능 데이터를 저장한다. +Scouter wants to collect bigger data and analyze each service transaction(request). +so Scouter should control a lot of data. That’s why SCOUTER save service performance and profile data on compressed files. -SCOUTER wants to collect bigger data and analyze each service transaction(request). -so SCOUTER should control a lot of data. That’s why SCOUTER save service performance and profile data on compressed files. - -## SCOUTER 타겟 시스템에 대한 개별요청을 추적한다.(XLOG) +## 타겟 시스템에 대한 개별 요청을 추적 Every service call is individually traced(profiled) and saved it. It is possible with the compressed archiving and standalone clients. -## SCOUTER는 진행중인 스택덤프를 분석한다. +## Scouter 진행중인 스택덤프를 분석한다. Sometimes it is not clear to understand the performance problem in a separate thread information. At that time, we have to think about different way. If we collect full thread stacks in many times and analyze the stacks together, we could get an another chance to solve the performance problem. (coming soon) diff --git a/scouter.document/server/Console-Mode-Running.md b/scouter.document/server/Console-Mode-Running.md index a18f8b9f5..eeb108973 100644 --- a/scouter.document/server/Console-Mode-Running.md +++ b/scouter.document/server/Console-Mode-Running.md @@ -1,23 +1,21 @@ # Console Mode Running ![Englsh](https://img.shields.io/badge/language-English-red.svg) [![Korean](https://img.shields.io/badge/language-Korean-blue.svg)](Console-Mode-Running_kr.md) -Scouter는 특별한 환경(클라이언트를 사용할 수 없는 환경)을 위해 -콘솔모드로 모니터링 할 수 있는 방법이 제공된다. 이렇게 모니터링하는 기능이 부족하기 때문에 -임시로만 사용해야한다. +It is also provided console-mode-monitoring. But remember this method is temporary for rarely special cases. -다음과 같은 경우에 콘솔 모드를 사용할 수 있다. +For example, you can use console-mode-monitoring, -1. 서버와 클라이언트 사이의 방화벽이 개방되지 않은 경우 -2. 서버와 클라이언트 사이에 네트웍 환경이 좋지 않은 경우 +1. When the communication is not establishable between Server and Client due to Company firewall policy +2. Network quality is not very good, -클라이언트가 서버에 접속할 수 없는 상황에서 유용하게 사용될 수 있다. +For any situations when Client is useless not to connect to Server directly, console-mode-monitoring is useful. -서버를 실행할때 startcon.sh를 사용하면 다음과 같이 명령을 위한 프롬프트가 나타난다. +Use startcon.sh startup script to use Scouter as console-mode-monitoring. You'll see command line prompt like, ![Tomcat](../img/server/scouter_console.png) -참고할 점은 +Important points, -1. 콘솔을 정지하면 서버가 정지된다. 따라서 데몬으로 실행전환하고자 하면 재기동해야한다. -2. 콘솔모드라도 모든기능은 일반기동과 동일하게 동작한다. +1. When console-mode was stopped, Server will go down. To startup as daemon mode, you should run the server again. +2. Console mode supports full features. diff --git a/scouter.document/tech/Counting-Visit-Users.md b/scouter.document/tech/Counting-Visit-Users.md index 14cb3774e..8483f835c 100644 --- a/scouter.document/tech/Counting-Visit-Users.md +++ b/scouter.document/tech/Counting-Visit-Users.md @@ -1,38 +1,33 @@ -# HyperLogLog를 이용하여 방문자 계산하기 +# Getting unique visitors with HyperLogLog algorithm ![Englsh](https://img.shields.io/badge/language-English-red.svg) [![Korean](https://img.shields.io/badge/language-Korean-blue.svg)](Counting-Visit-Users_kr.md) -하룻동안 방문한 사용자는 시스템의 성능과 비즈니스 연관도를 설명하기 위한 중요한 지표이다. -성능적으로 보면 동시 사용자가 중요한 기준이 되지만 -IT담당자가 아닌경우에 동시사용자 보다는 방문사용자를 많이 사용한다. +'Visitors' is important metric to understand relationship between business frequency and system performance. Just with performance, the highest concurrent users are in case. But with the perspective of business visitors are more important than concurrent user. -## 방문사용자를 어떻게 측정할 것인가? +## How to count unique visitors? -방문사용자는 측정하기 위해서는 먼저 서버 시스템에서 사용자를 유일하게 식별할 수 있어야 한다. -사용자를 유일하게 식별하는 방법은 로그인 아이디나 IP를 사용하거나JSESSIONID혹은 그와 유사한 사용자별 고유의 식별아이디를 사용할 수 있다. +When it count the unique vistors, each user's request should be distinguishable. There are serveral ways to give uniqueness to each request like using login ID, IP address, checking special cookie (like JSESSIONID), or other methodologies giving unique ID. -각 방법마다 약간의 단점들이 있다. -* 로그인 - 사용자가 로그인하기 전에는 측정이 않됨 -* IP - 서버에서는 식별이안됨 -* JSESSIONID - 한 사용자가 여러번 반복 로그인할경우 사용자가 과댜계산될 수 있음 -* COOKIE - 시스템에 Cookie를 추가하기 때문에 영향을 줄 수 있음 -이러한 단점들을 고려하여 서비스 요청별 사용자를 식별한다. +Each way has pros also cons, +* Login ID - It will not be counted until user logged in the system. +* IP Address - In some environments getting client's IP adress is not possible. +* JSESSIONID - Multiple accessing of system within counting period will be counted redundantly. +* COOKIE - Adopting new cookie can make system more complexed. -그런데 방문자 측정에 가장 어려운점은 사용자 식별보다는 실제 숫자를 계산해 내는 것에 있다. -HashSet과 같은 클래스를 통해 몇명인지를 계산하는것이 정확하지만 메모리를 많이 사용하기 때문에 쉽지 않다. +You should understand all of this technique and cons to count exact visiting number. -이러한 문제를 해결하고 적은 메모리로 방문사용자를 계산할 수 있는 적합한 알고리즘이 HyperLogLog이다. +For the developer it is also difficult to implement effective source code to count up, because counting is very slow and greedy. For example counting with HashSet collection consumes very much memory. -자세한 설명은 HyperLogLog에 대한 설명을 참조하고 여기서는 샘플 프로그램을 통해서 방문 사용자 계산하는 방법을 시뮬레이션 해본다. +The HyperLogLog is proper algorithm not consuming so much memory to estimate or approximate unique visitors. Please refer to the other articles or documents for more about HyperLogLog. Here is the simulation of it. -## 소스 코드 +## Example -TestHLL은 천만명의 사용자를 가정하고 HyperLogLog를 사용하여 계산하면 어떤 결과가 나오는지를 시뮬레이션 해보았다. +TestHLL example had simulated the situation of 10 million users accessing the system, and caculated result with HyperLogLog. -* realSet => 실제 방문자를 계산한다(기준값) -* all =>전체 -* even => 짝수번호 사용자 -* odd => 홀수 번호 사용자 -* sum => all + even + odd +* realSet : Calculate real visitor +* all : means total users +* even : user with even number ticket +* odd : user with odd number ticket +* sum = all + even + odd ``` import java.util.HashSet; @@ -80,7 +75,7 @@ public class TestHLL { } ``` -## 실행결과 +## Results ``` 10 => all=10 even=5 odd=5 sum=10 20 => all=20 even=10 odd=10 sum=20 @@ -138,11 +133,9 @@ public class TestHLL { 9000000 => all=8986135 even=4495527 odd=4506118 sum=8986135 10000000 => all=9984382 even=4992773 odd=5001554 sum=9984382 ``` -천만명이 방문한다고 가정하고 각 구간별로 HyperLogLog알고리즘으로 계산된 방문자의 숫자이다. -어느정도 인정한만한 오차범위에서 값을 계산해 내는 것을 볼 수 있다. +Each row represents the simulation result to 10 million users. As HyperLogLog algorithm is using statistical approach, the counring number is not absolutly exact number, provides margin of error. But the margin rate is low and acceptable. -특히 부분 사용자를 통해 전체를 구할 수 있다. -위의 TestHLL 소스코드를 보면 sum에 all 변수의 사용자 값을 모두 add하여도 sum ==all을 확인 할 수 있다. +And with the number of part user, we can get all user value. You can find that 'sum' variable is equivalent to 'all' variable. ``` HyperLogLog sum = new HyperLogLog(20); sum.addAll(even); @@ -150,8 +143,6 @@ HyperLogLog sum = new HyperLogLog(20); sum.addAll(all); ``` -이것은 서버별 방문자를 계산한 다음 업무별 사용자를 다시 계산 할 수 있는 의미가 된다. +This means counting users for each business function and each server. -방문사용자는 시스템의 성능이나 장애가 비즈니스에 미치는 영향도를 파악하는데 중요한 지표이다. -하지만 그 값이 완전히 정교할 필요는 없으며 어떠한 방법을 사용하여도 실제의 값을 구할 수는 없다. -어차피 대략의 규모를 효과적으로 파악하는 것이 중요하다. +Unique visitors is important because it has relationship between incoming request and system performance. And it is important to apply estimating or approximating algorithm with acceptable margin of errors. diff --git a/scouter.document/tech/JDBC-Connection-Leak-Trace.md b/scouter.document/tech/JDBC-Connection-Leak-Trace.md index 52d736e47..27d259fbd 100644 --- a/scouter.document/tech/JDBC-Connection-Leak-Trace.md +++ b/scouter.document/tech/JDBC-Connection-Leak-Trace.md @@ -1,38 +1,34 @@ -# JDBC Connection Leak Trace +# Trace of JDBC Connection Leak ![Englsh](https://img.shields.io/badge/language-English-red.svg) [![Korean](https://img.shields.io/badge/language-Korean-blue.svg)](JDBC-Connection-Leak-Trace_kr.md) -JDBC Connection을 추적의 목적은 대표적인 Connection 관련 문제를 감지하기 위한 것이다. +The purpose of tracking JDBC connection is most common methodology to detect DB connection problem. ## Connection Leak -
Connection을 pool에 반환하지 않는 문제이다. Statement나 ResultSet을 close하지 않는 문제는 크게 문제 되지 않는다.(문제 될 경우도 있다.), JDBC드라이버가 어느정도 해결해 주고 있다. 
하지만 Connection미반환 문제는 반드시 해결하는 문제이다. +It is the problem not to return the leased connection to the connection pool. Not closing Statement or ResultSet is not so siginificant in most of cases;JDBC driver is handling this. But not returning problem must be analyzed and solved. ## getConnection Delay -Connecttion Pool크기가 작아 부족한 경우 자주 발생하며 
Connection을 새로 맺는 경우에도 발생한다. +It tends to occur under the condition of low number of connection pool, and also making new connection. ## setAutoCommit True -setAutoCommit가 true로 setting되면 매 SQL마다 자동으로 Commit이 DB에 전달된다. -당연히 성능에 문제가 된다. +Enabling setAutoCommit to true, each SQL execution of statement is firing commit signal to DB automatically. This can be performance problem. ## Too many commits -"setAutoCommit"과 연관된 문제이기는 하지만 setAutoCommit이 False인 경우에서 개발자들이 명시적으로 빈번하게 - 커밋을 호출하는 경우가 있다(보통은 프레임웍에서 중간에 커밋하지 못하도록 막아준다.) +The explicit calling of commit() function on source code can drop down system performance, though setAutoCommit is false. (Nomally framework cut off this explicit commits in the middle) ## Commit Delay -너무 많은 데이터를 입력하고 커밋하는 경우에 커밋지연이 발생하는데 -데이터베이스 쪽에서 상세하게 분석해야 한다. +Commit delay is occurring due to the massive data insertion. This should be digging down on DB side. ## Close Delay -보통 WAS환경에서는 Close가 호출된다. Connection이 Pool에 반환된다. -Close에서 delay가 발생하면 Pool의 상태를 분석해야한다 +On the common environment using connection pool handled by WAS, close() function was called to return leased connection to connection pool. When close() is called frequently you should check the status of connection pool. -이러한 문제가 clear되었다고 확신한다면 Connection 추적을 off 한다. +Most of problems are solved, you can turn off tracking function. ``` profile_connection_open_enabled=false (기본값: true) ``` -톰켓의 경우에는 자동으로 Connection을 추적한다 하지만 MyBatis같은 프레임웍에서 다른 DataSource 를 사용하는 경우에는 명시적으로 설정한다. +Tomcat Agent is using tracking connection pool by default. If you are not using WAS pool, add explicit tracking option. -아래는 MyBastis설정이다. +Let's check this case out, below is for MyBatis framework, ``` ``` -이런 경우 org.springframework.jdbc.datasource.SimpleDriverDataSource을 다운받아서 역컴파일 해본다. -그러면 아래와 같이 AbstractDriverBasedDataSource을 상속하고 있고 자체에는 getConnection이 없는 것을 확인할 수 있다. +With this case, decompile org.springframework.jdbc.datasource.SimpleDriverDataSource class. As you can see below, this class is extends AbstractDriverBasedDataSource and it doesn't have getConnection() function. ``` package org.springframework.jdbc.datasource; @@ -54,12 +49,11 @@ public class SimpleDriverDataSource extends AbstractDriverBasedDataSource { … ``` -다시 AbstractDriverBasedDataSource을 다운받아서 확인하면 getConnection이 있는 것을 확인할 수 있다. -아래의 옵션을 에이전트에 추가하고 재기동하면 +Let's check AbstractDriverBasedDataSource. It has getConnection() function. This is the case application is reponsible for creating and managing connection pool inside of it. JDBC tracking option is needed. ``` hook_connection_open_patterns=org.springframework.jdbc.datasource.AbstractDriverBasedDataSource.getConnection ``` -프로파일에서 아래와 같은 내용을 확인 할 수 있다. +In the profiling data, you can see below logs, ``` - [000002] 22:49:09.445 0 0 OPEN-DBC jdbc:fake: diff --git a/scouter.document/tech/Why-Recent-User.md b/scouter.document/tech/Why-Recent-User.md index 29e2b8537..7ba43d79d 100644 --- a/scouter.document/tech/Why-Recent-User.md +++ b/scouter.document/tech/Why-Recent-User.md @@ -1,27 +1,19 @@ # Why Recent User ![Englsh](https://img.shields.io/badge/language-English-red.svg) [![Korean](https://img.shields.io/badge/language-Korean-blue.svg)](Why-Recent-User_kr.md) -사용자는 크게 방문자, 동시사용자, 액티브 사용자로 구분할 수 있다. 각 사용자 개념은 다른 목적을 가지고 활용된다. -그런데 시스템의 성능을 이야기 할 때는 가장 중요한 숫자가 동시 사용자이다. +The concept of 'users' is seperated by Visitors, Concurrent Users, and Active Users. Each one has different purpose to tell about system. With the view of performance, concurrent users are most important. -### 동시 사용자 의미 -동시 사용자는 어떤 싯점에 시스템을 접속해서 사용하고 있는 사람을 말한다. -어떤사람은 블러그에 글을 입력하는 사람, 어떤 사람은 서버에 submit을 호출한 사람, 서버의 응답을 기다리는 사람등이 모두 포함된다. -상담원이 500명인 시스템은 동시사용자가 500명일 가능성이 높다. -상담중일수도 있고 현재 값을 입력중일 수도 있지만 사용자 입장에서 현재 시스템을 사용하고 있다는 것이다. +### Concurrent Users +Concurrent users represents the number of users who are connecting to the system at the same time. Someone is writing text on the client text box, and anothers are sending submit() signal, and others are waiting the reponse. All of these users are counted to concurrent users. For instance, let's assume some CRM center has 500 tellers. At the peak time, concurrent users of CRM system is above 500. 500 people is telling via their telephone and others are waiting on the line. -### 측정의 어려움 -그런데 이 동시 사용자는 중요한 수치임에도 측정하기가 만만치 않다. -Little’s Rule을 이용하여 응답시간과 TPS, 호출 간격등을 어렵게 측정하여 계산식으로 뽑아내어도 측정방식의 편차로 인해 그래프가 틀어지는 경우가 많다. -특히 막상 장애가 났을때 사용자가 증가하여 부하를 받고 응답이 늦어졌는지 아니면 반대로 응답이 지연되여 동시사용자가 증가한 것으로 나타난 것인지를 판단하기 어렵게된다. -이것은 동시사용자가 응답시간이 포함된 계산식에 의해 측정되기 때문에 나타나는 현상이다. +### Difficulty on Measurement +Though concurrent users are important metric to system, it is not easy to calculate and measure. With Little's Rule caculation of reponse time, TPS, think time, the equation result may not be exact. Because reponse time is affecting factor. There is no clear evidence to tell the preceding relationship between lateness of reponse time and arisen concurrent users. + +### Recent Users +Scouter measures recently visited user count, and display it as 'RecentUsers'. The time period of measurement is important unit in RecentUsers concept. By default, Scouter is measuring the number of unique vistors of last 5 minutes. But some cases like performance test which has lower thant 5 mins of think time, this should be modifiable. + +You can specify this time period by modifying conter_recentuser_valid_ms. -### 그래서 SCOUTER는 최근사용자(RecentUser)를 사용한다. -Scouter는 각 인스턴스별로 최근방문사용자를 측정한고 이것을 RecentUser라고 표기한다. -이 경우 최근이라는 개념을 얼마의 시간으로 얼마로 할것인가의 문제가 발생한다. -기본은 최근 5분동안 방문한 Unique사용자를 Recent User로 계산한다. -그런데 3분단위로 성능테스팅을 한다면 5분은 너무 긴시간이다. 따라서 성능테스트 시점에는 시간을 줄여 주어야한다. -이때 사용하는 파라미터가 max_think_time이다 이시간 이내에 다시 트랜잭션을 발생시킨 사용자의 수만 최근 사용자로 측정된다. ```properties counter_recentuser_valid_ms=300000 ``` diff --git a/scouter.document/use-case/NON-HTTP-Service-Trace.md b/scouter.document/use-case/NON-HTTP-Service-Trace.md index bf9e0ce9f..9d653c39d 100644 --- a/scouter.document/use-case/NON-HTTP-Service-Trace.md +++ b/scouter.document/use-case/NON-HTTP-Service-Trace.md @@ -1,19 +1,15 @@ # NON HTTP Server Trace ![Englsh](https://img.shields.io/badge/language-English-red.svg) [![Korean](https://img.shields.io/badge/language-Korean-blue.svg)](NON-HTTP-Service-Trace_kr.md) -WAS(ex Tomcat)가 아닌 서버에서 서비스를 추적하는 방법을 설명한다. - -서비스가 시작되는 메소드를 hook_service_patterns에 등록해야한다. +This article explains non-hosted-by-WAS service. You should add entrypoint method of your service in hook_service_patterns configuration. ``` hook_service_patterns=com.mypkg.MyClass.myservice ``` -형식으로 풀패키지명과 클래스/메소드 이름까지 설정한다. - -그런데 서비스 시작 부분을 찾기 위해서는 분석이 필요하다. +Configuration format is consisted with full package paths and service method name. Analysis may be needed to determine what the entry point is. -참고) +Here is live example, ``` _trace_auto_service_enabled=false _trace_auto_service_backstack_enabled=true @@ -25,26 +21,24 @@ hook_method_access_protected_enabled=false hook_method_access_none_enabled=false hook_method_ignore_prefixes=get,set ``` -### 단계 1 -먼저 프로파일 대상 클래스 들을 모두 지정한다 대게는 업무나 프레임웍 패키지를 모두 지정한다. +### Step 1 +At first, add package name patterns of business or framework package, which will be profiling target of Scouter Agent. ``` hook_method_patterns=com.mypkg*.*, org.other*.* ``` -단 메소드가 많을 경우 hook_method_xxx옵션들을 이용하여 필터링 할 수 있다.. +You can narrow down profiling range by adding hook_method_xxx option(s). -### 단계 2 -hook_method는 단지 프로파일링할 대상 메소드를 지정하는 것이다. 그런데 서비스 추적이 시작되지 않으면 프로파일을 추적하지 않는다 -이때 자동 서비스 추적을 enable한다. +### Step 2 +hook_method options are used for specifying which methods should be traced. Scouter will start to profile that methods after the service trace is triggered on. So auto trace of service startup should be enabled below, ``` _trace_auto_service_enabled=true ``` -### 단계 3 -**프로세스를 재기동하고 서비스를 호출**하면 -종료되지 않는 서비스들을 볼 수 있다. 혹은 종료되었다면 XLog에서 상세한 정보를 조회할 수 있다. +### Step 3 +You can check the termination status or termination information on XLog, after service process was restarted. -### 단계 4 -찾아진 서비스 시작점을 hook_service_patterns 지정하고 hook_method 나 enable_auto_service_trace 옵션을 제거한다. +### Step 4 +Add found service entrypoint to hook_service_patterns, remove hook_method or enable_auto_service_trace option. ``` hook_service_patterns=com.mypkg.MyClass.myservice ``` diff --git a/scouter.document/use-case/Simulate-DB-Lock.md b/scouter.document/use-case/Simulate-DB-Lock.md index 4e32ee9d4..1cee48183 100644 --- a/scouter.document/use-case/Simulate-DB-Lock.md +++ b/scouter.document/use-case/Simulate-DB-Lock.md @@ -1,23 +1,22 @@ # Simulate DB Lock ![Englsh](https://img.shields.io/badge/language-English-red.svg) [![Korean](https://img.shields.io/badge/language-Korean-blue.svg)](Simulate-DB-Lock_kr.md) -본 문서는 간단하게 DB에 update lock이 걸렸을때 어떻게 모니터링 되는지를 시뮬레이션한다. -기본적인 환경구성은 [Getting Start Profile SQL](../main/Getting-Start-Profile-SQL.md)을 참조한다. - -### 환경구성하기 -1. JDK7설치 ( [Getting Started](../main/Getting-Started.md) 참고 ) -2. HSQLDB설치 ( [Getting Start Profile SQL](../main/Getting-Start-Profile-SQL.md) 참고 ) -3. Tomcat설치 ( [Getting Started](../main/Getting-Started.md) 참고 ) -4. JMeter설치 ( [Getting Start Profile SQL](../main/Getting-Start-Profile-SQL.md) 참고 ) -5. Scouter설치 ( [Getting Started](../main/Getting-Started.md) 참고 ) -6. Tomcat환경구성 - - DataSource설정 ([Getting Start Profile SQL](../main/Getting-Start-Profile-SQL.md) 참고) - - Sample jsp설치 - [create.jsp](https://github.com/scouter-project/scouter-help/blob/master/misc/test-jsp/create.jsp), [sql.jsp](https://github.com/scouter-project/scouter-help/blob/master/misc/test-jsp/sql.jsp), [lock.jsp](https://github.com/scouter-project/scouter-help/blob/master/misc/test-jsp/lock.jsp) 3개의 파일을 ${TOMCAT_HOME}/webapps/ROOT/.로 복사한다. - - - Scouter Agent설정 ( [Getting Started](./Getting-Started) 참고 ) - -7. 모두 기동 +This article describes how to simulate and monitor DB update lock with Scouter environment. Installation and configuration guide is [here](../main/Getting-Start-Profile-SQL.md). + +### Installation Guide +1. JDK7 Installation ( [Getting Started](../main/Getting-Started.md) ) +2. HSQLDB Installation ( [Getting Start Profile SQL](../main/Getting-Start-Profile-SQL.md) ) +3. Tomcat Installation ( [Getting Started](../main/Getting-Started.md) ) +4. JMeter Installation ( [Getting Start Profile SQL](../main/Getting-Start-Profile-SQL.md) ) +5. Scouter Installation ( [Getting Started](../main/Getting-Started.md) ) +6. Tomcat Configuration + - DataSource ([Getting Start Profile SQL](../main/Getting-Start-Profile-SQL.md)) + - Deploy sample jsp pages, + [create.jsp](https://github.com/scouter-project/scouter-help/blob/master/misc/test-jsp/create.jsp), [sql.jsp](https://github.com/scouter-project/scouter-help/blob/master/misc/test-jsp/sql.jsp), [lock.jsp](https://github.com/scouter-project/scouter-help/blob/master/misc/test-jsp/lock.jsp) Copy these 3 files to ${TOMCAT_HOME}/webapps/ROOT/ + + - Scouter Agent configuration ( [Getting Started](./Getting-Started) ) + +7. Startup all together - HSQLDB ``` runServer.bat --database.0 file:mydb --dbname.0 xdb @@ -31,45 +30,44 @@ startup.bat - Scouter Server - Scouter Client -8. Scouter Client 모니터링 +8. Scouter Client monitoring -### JMeter실행(부하발생) -JMeter를 실행하고 앞에서 설치했던 sql.jsp롤 호출하여 부하를 발생하는 테스트를 수행한다. +### Run JMeter (Generating load) +Do load test about sql.jsp via JMeter. ->죄측윈도우 >> Test Plan(우측마우스) >> Add >> Threads(Users) >> setUp Thread group +>Left Window >> Test Plan (right mouse button) >> Add >> Threads(Users) >> SetUp Thread group ![Set Users](../img/client/jmeter/set_users.png) -> setUp Thread group(우측마우스) >> Add >> Sampler >> HTTP Request +>> setUp Thread group(right mouse button) >> Add >> Sampler >> HTTP Request ![HTTP Request](../img/client/jmeter/http_request.png) -화면에서 다음값을 입력한다. +Input these paramters, * server ip : 127.0.0.1 * port : 8080 * Path : /sql.jsp -실행버튼을 눌러 실행하면 다음과 같은 내용을 Scotuer클라이언트에서 볼 수 있다. +You can see the screen like below after clicking run button on JMeter. ![Scouter](../img/client/jmeter/scouter_client.png) -> 추가플러그인 설치 : http://jmeter-plugins.org +> JMeter plugins : http://jmeter-plugins.org -### Lock 유발시키기 -lock.jsp를 호출하여 락을 유발시킬수 있다. 파라미터 "t"값을 통해 락 시간을 조절할 수 있다. +### Simulate DB Update Lock +DB update lock will occur on lock.jsp page. Call this page with paramter 't' value to determine locking time. ![lock.jsp](../img/client/jmeter/lock.png) -그러면 Scouter화면에서 XLog와 다른 차트를 통해 락이 발생하는 것을 볼 수 있다. +You can see DB lock information at XLog and other chart on Scouter Client. ![Scouter](../img/client/jmeter/scouter_lock.png) -위 화면에서 어느 구간에서 지연이 발생하고 있는지 분석하기위해서는 -두가지 방법을 활용한다. 왼쪽 하단의 액티브서비스 리스트를 더블클릭해서 조회하면 진행중인 서비스 리스트를 볼수 있고 상세 정보를 조회할수 있다. 아래화면에서 두데이터 모두 많은 서비스들이 update SQL을 수행하고 있음을 알 수 있다. +There are two steps to identify which business is making DB lock problem. First is checking active service list on left bottom on Client. Two is analyzing detailed transaction information of running service. After double clicking red bar, the detailed transaction information popup is displayed. ![active list](../img/client/jmeter/analyze_active_list.png) ![active detail](../img/client/jmeter/analyze_active_detail.png) -만약 서비스가 종료되면 XLog화면에서 프로파일을 조회함으로 문제를 분석할 수있다. +After the termination of service, profiles of tested transaction will be displayed on XLog chart. ![profile](../img/client/jmeter/analyze_profile.png) diff --git a/scouter.document/use-case/TagCounting-Analysis.md b/scouter.document/use-case/TagCounting-Analysis.md index 71f5d385a..0f0e3bf8e 100644 --- a/scouter.document/use-case/TagCounting-Analysis.md +++ b/scouter.document/use-case/TagCounting-Analysis.md @@ -1,17 +1,11 @@ -# TagCounting 분석기법 +# TagCounting Analysis ![Englsh](https://img.shields.io/badge/language-English-red.svg) [![Korean](https://img.shields.io/badge/language-Korean-blue.svg)](TagCounting-Analysis_kr.md) -서비스 성능은 응답시간과 처리건수로 측정된다. 더 정확히 이야기하면 -단위 시간당 호출된 건수와 이것을 처리하기 위해 걸린시간을 기준으로 서비스의 성능을 관리한다. +Peformance of service is measured by response time and the number of transactions. Speaking more accurately, by processed transactions per specified time unit and its processing time. -그런데 실제 환경에서 서비스는 다수의 사용자에 의해 다수의 서비스가 동시에 호출되며 이 서비스 들은 다수의 서로 다른 자원들을 공유하거나 배타적으로 사용한다. -따라서 성능의 문제를 분석할때는 이들 서비스를 개별로 분석해야만 서비스의 현황을 정확히 파악할 수 있다. 이렇게 만들어진것이 응답시간 분포도(XLOG)이다. -그런데 이들 서비스 들은 독립된 공간에서 혼자 실행되는것이 아니고 서로다를 서비스들과 어떤 자원들을 공유하며 실행되기 때문에 통계적 분석이 (또한) 필요하게 된다. 이것을 위해 만들어진 기능은 TagCounting이다. +When many users use production system, each service on same system uses shared computational resources exclusively or not. To analyze the performance of system, we should take look at each service independently. XLog chart can help this. We also need statistical analysis as the usage of shared resources of one system can affect the other. TagCounting makes this more simple. -하나의 서비스가 수행되면 이것을 트랜잭션이라고 부른다. -하나의 트랜잭션에는 여러가지 정보들을 가지고 있다. 어떤URL인지, 어떤 IP에서 호출했는지 혹은 어떤 사용자가 호출했는지 등등 이러한 정보들을 각가 테그라고 부른다. -예를 들어 ip라는 테그에는 값이 192.168.10.10 이러한 방식으로 각 테그에는 값들이 포함되어있다. -이해를 돕기 위해 트랜잭션 하나를 예로 들어보자 +We call the result of one service as transaction. A transaction contains information about itself, URL of it, client IP addrss, user information and so on. Each information is called as tag. For example, IP address tag has the value of 192.168.10.10. Let's look at example transaction to clarify, ``` ► objName = /sjhost/node1 @@ -32,19 +26,17 @@ ► login = joe ``` -테그카운팅이란 각 속성즉 테그/값 별로 수행 통계를 분석하는 것이다. -예를 들어 ipaddr/192.168.10.10 이 테그를 가진 트랜잭션이 금일 몇개나 발생했는지 파악해 본다. 또는 userAgent/Java/1.7.0_55 이런 테그를 가진 트랜잭션이 몇개나 발생했는지 하루 추의를 분석한다. -이렇게 분석해 보면 공통점을 발견하거나 공통점이 없다는 확신을 할 수 있게 된다. +It is TagCounting that analyze processing statistics with each tag and their value. For exmaple, list up a tag with 'ipaddr/192.168.10.10' of today to analyze a client behavior. Or sum up the number of transactions with tag 'userAgent/Java/1.7.0_55' of a day. With this action we can have intuitive view of characteristics of transactions. + -전체 건수를 보여준다. +Below is the statistical analysis example of 24 hours. ![TagCounting #1](../img/client/tagcnt_1.png) -차트에서 왼쪽마우스로 드레그하면 해당 구간의시간을 상세히 볼수 있다. -그리고 하나의 바를 더블클릭하면 아래에 세부 리스트를 볼수 있다. +You can get the detailed graph of a time period specified by mouse dragging. And double-clicking any bar will show detaied transactions list. ![TagCounting #2](../img/client/tagcnt_2.png) -세부리스트에서 특정 열을 클릭하면 프로파일 정보를 볼수 있다. +You can check profiled information on each row. ![TagCounting #3](../img/client/tagcnt_3.png) -특정테그/값을 선택하여 수행현황을 조회할 수 있다. -![TagCounting #4](../img/client/tagcnt_4.png) \ No newline at end of file +Also select specific tag or its value. +![TagCounting #4](../img/client/tagcnt_4.png) diff --git a/scouter.document/use-case/XLog-Case3.md b/scouter.document/use-case/XLog-Case3.md index fedc6a239..6ee32e8e5 100644 --- a/scouter.document/use-case/XLog-Case3.md +++ b/scouter.document/use-case/XLog-Case3.md @@ -1,20 +1,12 @@ -# XLog Case3 - Undestand Horizontal +# XLog Case 3 - Undestand about Horizontal Pattern ![Englsh](https://img.shields.io/badge/language-English-red.svg) [![Korean](https://img.shields.io/badge/language-Korean-blue.svg)](XLog-Case3_kr.md) -응답분포(XLOG)의 대표적인 패턴중에 하나는 가로라인이 나타나는 것이다. -가로 라인은 어떤 트랜잭션의 응답시간이 정형한 것이다. -보통 자원에 대한 획득을 위해 일정시간 WAIT이 발생하는 경우 가로라인이 형성된다. -예를 들어 어떤자원을 조회하고 실패하면 3초 기다렸다 다시 조회하는 경우 3초,6초,9초의 지연이 발생하고 -화면에서는 3초간격의 라인이 형성된다. +One of the well known problem patterns on XLog is markers ditributed horizontally. This means some businesses or services have statical response time. Most of cases are waiting time for obtaining usage rights for some resources. For example, assume that waiting time for any resource is 3 seconds. Then the horizontal line will be displayed every 3 seconds (3 sec, 6 sec, 9 sec, ...). -또는 테스트 할때 외부 연계를 시물레이션하기 위해 일정시간의 Sleep을 걸어두는 경우에도 -라인이 형성된다. +And the line also will be appeared explicit sleep time to simulate the communication time for interaction with external system. ![Horizontal Line](../img/client/xlog_horizontal.png) -물로 어떤 경우에는 일정시간을 기다렸다 처리하도록 고의로 서비스 지연을 유도하는 경우도 있지만 -최근의 인터넷 서비 환경에서는 그렇게 처리하지 않는다. 과거에는 부족한 자원 때문에 서서히 처리하도록 프로그램했지만 -요즘에는 자원을 늘리는 방식을 택한다. +In old time application logic was implemented with explicitly postponing algorithm due to the shortage of computaional power or resources. Now a days, it is very rare cases because making system more powerful is easy. -따라서 가로 라인이 만들어진다는 것은 어떤 이유가 존재한다. -반드시 운영자는 평상시 가로라인이 형성된다면 그 이유를 명확히 확인해야 한다. \ No newline at end of file +Because horizontal line means problem, it must be analyzed. diff --git a/scouter.document/use-case/XLog-Case3_kr.md b/scouter.document/use-case/XLog-Case3_kr.md index d48872310..a64140a1a 100644 --- a/scouter.document/use-case/XLog-Case3_kr.md +++ b/scouter.document/use-case/XLog-Case3_kr.md @@ -1,20 +1,15 @@ # XLog Case3 - 수평 라인 형태의 이해 [![Englsh](https://img.shields.io/badge/language-English-red.svg)](XLog-Case3.md) ![Korean](https://img.shields.io/badge/language-Korean-blue.svg) -응답분포(XLOG)의 대표적인 패턴중에 하나는 가로라인이 나타나는 것이다. -가로 라인은 어떤 트랜잭션의 응답시간이 정형한 것이다. -보통 자원에 대한 획득을 위해 일정시간 WAIT이 발생하는 경우 가로라인이 형성된다. -예를 들어 어떤자원을 조회하고 실패하면 3초 기다렸다 다시 조회하는 경우 3초,6초,9초의 지연이 발생하고 -화면에서는 3초간격의 라인이 형성된다. +응답 분포 차트(XLog)에 나타나는 대표적인 패턴 중 하나는 가로 라인이 나타나는 것이다. +가로 라인은 트랜잭션의 처리 중 자원 획득 시 Wait 값과 같은 일정한 지연이 있을 때 발생한다. +예를 들어 어떤 자원을 조회하고 실패하면 3초 기다렸다 다시 조회하는 경우 3초, 6초, 9초의 지연이 발생하고 +화면에서는 3초 간격의 라인이 형성된다. -또는 테스트 할때 외부 연계를 시물레이션하기 위해 일정시간의 Sleep을 걸어두는 경우에도 -라인이 형성된다. +또는 테스트 할때 외부 연계를 시물레이션하기 위해 일정시간의 Sleep 을 걸어두는 경우에도 라인이 형성될 수 있다. ![Horizontal Line](../img/client/xlog_horizontal.png) -물로 어떤 경우에는 일정시간을 기다렸다 처리하도록 고의로 서비스 지연을 유도하는 경우도 있지만 -최근의 인터넷 서비 환경에서는 그렇게 처리하지 않는다. 과거에는 부족한 자원 때문에 서서히 처리하도록 프로그램했지만 -요즘에는 자원을 늘리는 방식을 택한다. +과거에는 단시간에 시스템의 처리 능력을 향상시킬 수 없어서 고의적으로 처리 지연을 유도하는 경우도 있었지만 최근에는 IaaS 환경과 같이 지연 처리하지 않고 자원을 바로 늘리는 방식을 택한다. -따라서 가로 라인이 만들어진다는 것은 어떤 이유가 존재한다. -반드시 운영자는 평상시 가로라인이 형성된다면 그 이유를 명확히 확인해야 한다. \ No newline at end of file +가로 라인이 만들어진다는 것은 원인이 항상 존재하며, 운영자는 가로라인이 관찰되면 반드시 그 원인을 파악하고 개선해야 한다. diff --git a/scouter.server/build-boot-jar-for-testing.xml b/scouter.server/build-boot-jar-for-testing.xml index c00d134ef..630c0c3fc 100644 --- a/scouter.server/build-boot-jar-for-testing.xml +++ b/scouter.server/build-boot-jar-for-testing.xml @@ -1,6 +1,6 @@ - + diff --git a/scouter.server/src/scouter/server/Configure.java b/scouter.server/src/scouter/server/Configure.java index 194f81af7..0850bcd19 100644 --- a/scouter.server/src/scouter/server/Configure.java +++ b/scouter.server/src/scouter/server/Configure.java @@ -127,6 +127,9 @@ public final static synchronized Configure getInstance() { //TagCount public boolean tagcnt_enabled = false; + + //Visitor Hourly + public boolean visitor_hourly_count_enabled = true; private Configure() { reload(false); @@ -272,6 +275,8 @@ private void apply() { this.tagcnt_enabled = getBoolean("tagcnt_enabled", false); + this.visitor_hourly_count_enabled = getBoolean("visitor_hourly_count_enabled", true); + this.net_tcp_service_pool_size = getInt("net_tcp_service_pool_size", 100); ConfObserver.exec(); diff --git a/scouter.server/src/scouter/server/core/VisitorCore.scala b/scouter.server/src/scouter/server/core/VisitorCore.scala index 559915da3..77579a2ee 100644 --- a/scouter.server/src/scouter/server/core/VisitorCore.scala +++ b/scouter.server/src/scouter/server/core/VisitorCore.scala @@ -35,6 +35,7 @@ import scouter.util.HashUtil import scouter.util.Hexa32 import scouter.server.util.EnumerScala import scouter.server.db.VisitorDB +import scouter.server.db.VisitorHourlyDB object VisitorCore { @@ -65,10 +66,14 @@ object VisitorCore { if (ok == false) { Logger.println("S208", 10, "VisitDay queue exceeded!!"); } - } + } + def process(objType: String, x: XLogPack) { VisitorDB.getNewObjType(objType).offer(x.userid) - VisitorDB.getNewObject(x.objHash).offer(x.userid) + VisitorDB.getNewObject(x.objHash).offer(x.userid) + if (Configure.getInstance.visitor_hourly_count_enabled) { + VisitorHourlyDB.getNewObject(x.objHash).offer(x.userid) + } } } diff --git a/scouter.server/src/scouter/server/db/DailyCounterWR.scala b/scouter.server/src/scouter/server/db/DailyCounterWR.scala index 5ed5dbc17..1ef1a0b3f 100644 --- a/scouter.server/src/scouter/server/db/DailyCounterWR.scala +++ b/scouter.server/src/scouter/server/db/DailyCounterWR.scala @@ -49,8 +49,9 @@ object DailyCounterWR { close(); open(Integer.toString(counterData.date)); } - if (index == null) { + if (index == null || writer == null || writer.dataFile == null || index.index == null) { OftenAction.act("DailyCounterWR", 10) { + closeForce(); queue.clear(); lastDateInt = 0; } @@ -96,6 +97,21 @@ object DailyCounterWR { writer = null; } + def closeForce() { + try { + if (index != null) index.closeForce(); + } catch { + case e: Throwable => e.printStackTrace + } + try { + if (writer != null) writer.closeForce(); + } catch { + case e: Throwable => e.printStackTrace + } + index = null; + writer = null; + } + def open(date: String) { try { val path = getDBPath(date); @@ -108,7 +124,10 @@ object DailyCounterWR { return; } catch { case e: Throwable => { - e.printStackTrace(); + index = null + writer = null + Logger.println("G103", e.getMessage()) + Logger.printStackTrace("G104", e) close() } } diff --git a/scouter.server/src/scouter/server/db/VisitorDB.scala b/scouter.server/src/scouter/server/db/VisitorDB.scala index 40f7dec17..be77f5b66 100644 --- a/scouter.server/src/scouter/server/db/VisitorDB.scala +++ b/scouter.server/src/scouter/server/db/VisitorDB.scala @@ -28,13 +28,14 @@ import scouter.server.util.cardinality.HyperLogLog import scouter.util.DateUtil import scouter.util.ThreadUtil import scouter.util.IntKeyLinkedMap - import java.io.File import scouter.util.FileUtil import scouter.util.HashUtil import scouter.util.Hexa32 import scouter.server.util.EnumerScala import scouter.server.core.CoreRun +import scouter.lang.value.ListValue +import scouter.lang.value.DecimalValue object VisitorDB { val rsd = 20 @@ -161,5 +162,14 @@ object VisitorDB { def getVisitorObject(date: String, objHash: Int): Long = { val h = load(date, Hexa32.toString32(objHash)) if (h == null) 0 else h.cardinality() + } + + def getMergedVisitorObject(date: String, objHashLv: ListValue): Long = { + val totalVisitor = new HyperLogLog(rsd) + EnumerScala.foreach(objHashLv, (obj: DecimalValue) => { + val h = load(date, Hexa32.toString32(obj.intValue())) + if (h != null) totalVisitor.addAll(h) + }) + totalVisitor.cardinality() } } diff --git a/scouter.server/src/scouter/server/db/VisitorHourlyDB.scala b/scouter.server/src/scouter/server/db/VisitorHourlyDB.scala new file mode 100644 index 000000000..0310eacd2 --- /dev/null +++ b/scouter.server/src/scouter/server/db/VisitorHourlyDB.scala @@ -0,0 +1,148 @@ +/* +* Copyright 2015 the original author or authors. + * @https://github.com/scouter-project/scouter + * + * Licensed 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 scouter.server.db; + +import java.io.File + +import scouter.lang.value.DecimalValue +import scouter.lang.value.ListValue +import scouter.server.util.EnumerScala +import scouter.server.util.ThreadScala +import scouter.server.util.cardinality.HyperLogLog +import scouter.util.DateUtil +import scouter.util.FileUtil +import scouter.util.Hexa32 +import scouter.util.IntKeyLinkedMap +import scouter.util.ThreadUtil + +object VisitorHourlyDB { + val rsd = 20 + + //Hourly Execute + ThreadScala.startDaemon("scouter.server.db.VisitorHourlyDB") { + var hourUnit = DateUtil.getHour(System.currentTimeMillis()) + while (DBCtr.running) { + var currentHour = DateUtil.getHour(System.currentTimeMillis()) + if (hourUnit != currentHour) { + hourUnit = currentHour + objHashHyperTable.clear() + } + flush() + ThreadUtil.sleep(1000) + } + } + private var lastFlush = System.currentTimeMillis() + private def flush() { + val now = System.currentTimeMillis() + if (now - lastFlush >= 10000) { + lastFlush = now + EnumerScala.foreach(objHashHyperTable.keys(), (h: Int) => { + try { + val hhl = objHashHyperTable.get(h) + if (hhl != null && hhl.dirty == true) { + hhl.dirty = false + save(getFileName(Hexa32.toString32(h)), hhl) + } + } catch { + case _: Throwable => + } + }) + } + } + + val objHashHyperTable = new IntKeyLinkedMap[HyperLogLog].setMax(500); + + def getNewObject(objHash: Int): HyperLogLog = { + var h = objHashHyperTable.get(objHash) + if (h == null) { + h = load(DateUtil.yyyymmdd(), getFileName(Hexa32.toString32(objHash))) + if (h == null) { + h = new HyperLogLog(rsd) + } + objHashHyperTable.put(objHash, h) + } + h.dirty = true + return h + } + + private def load(date: String, name: String): HyperLogLog = { + return load(date, System.currentTimeMillis(), name) + } + + private def load(date: String, time: Long, name: String): HyperLogLog = { + val path = getDBPath(date); + val f = new File(path); + if (f.exists() == false) + f.mkdirs(); + val file = new File(path + "/" + getFileName(name, time) + ".usr"); + if (file.exists()) { + val bytes = FileUtil.readAll(file) + HyperLogLog.build(bytes) + } else { + null + } + } + + private def save(name: String, hll: HyperLogLog) { + val path = getDBPath(DateUtil.yyyymmdd()); + val f = new File(path); + if (f.exists() == false) + f.mkdirs(); + val file = new File(path + "/" + name + ".usr"); + FileUtil.save(file, hll.getBytes()) + } + + private def getDBPath(date: String): String = { + val sb = new StringBuffer(); + sb.append(DBCtr.getRootPath()); + sb.append("/").append(date).append("/visit_hourly"); + return sb.toString(); + } + + private def getFileName(name : String) : String = { + return getFileName(name, System.currentTimeMillis()) + } + + private def getFileName(name : String, time : Long) : String = { + return name + "_" + DateUtil.getHour(time) + } + + def getVisitorObject(objHash: Int): Long = { + var h = objHashHyperTable.get(objHash) + if (h == null) { + h = load(DateUtil.yyyymmdd(), Hexa32.toString32(objHash)) + } + if (h == null) 0 else h.cardinality() + } + + def getVisitorObject(date: String, time: Long, objHash: Int): Long = { + val h = load(date, time, Hexa32.toString32(objHash)) + if (h == null) 0 else h.cardinality() + } + + def getMergedVisitorObject(date: String, time: Long, objHashLv: ListValue): Long = { + var cardinality = 0L; + val totalVisitor = new HyperLogLog(rsd) + EnumerScala.foreach(objHashLv, (obj: DecimalValue) => { + val h = load(date, time, Hexa32.toString32(obj.intValue())) + if (h != null) totalVisitor.addAll(h) + }) + totalVisitor.cardinality() + } +} diff --git a/scouter.server/src/scouter/server/db/counter/DailyCounterData.scala b/scouter.server/src/scouter/server/db/counter/DailyCounterData.scala index c53c15e0e..5a9b123a2 100644 --- a/scouter.server/src/scouter/server/db/counter/DailyCounterData.scala +++ b/scouter.server/src/scouter/server/db/counter/DailyCounterData.scala @@ -72,18 +72,23 @@ class DailyCounterData(fileName: String, mode: String) extends IClose { DailyCounterData.table.synchronized { if (this.refrence == 0) { DailyCounterData.table.remove(this.fileName); - try { - dataFile = FileUtil.close(dataFile); - } catch { - case e: Throwable => - e.printStackTrace(); - } + DailyCounterData.table.remove(DailyCounterData.preFixForWriter + this.fileName); + dataFile = FileUtil.close(dataFile); } else { this.refrence -= 1; } } } + def closeForce() { + DailyCounterData.table.synchronized { + DailyCounterData.table.remove(this.fileName); + DailyCounterData.table.remove(DailyCounterData.preFixForWriter + this.fileName); + dataFile = FileUtil.close(dataFile); + this.refrence = 0; + } + } + def read(offset: Long): Array[Byte] = { this.synchronized { try { diff --git a/scouter.server/src/scouter/server/db/counter/DailyCounterIndex.scala b/scouter.server/src/scouter/server/db/counter/DailyCounterIndex.scala index d99aeeb02..344de1a77 100644 --- a/scouter.server/src/scouter/server/db/counter/DailyCounterIndex.scala +++ b/scouter.server/src/scouter/server/db/counter/DailyCounterIndex.scala @@ -34,7 +34,7 @@ object DailyCounterIndex { index.refrence += 1; return index; } else { - index = new DailyCounterIndex(fileName); + index = new DailyCounterIndex(fileName) table.put(fileName, index); return index; } @@ -45,7 +45,7 @@ object DailyCounterIndex { class DailyCounterIndex(_fileName: String) extends IClose { var refrence = 0 val fileName = _fileName - var index: IndexKeyFile = null + var index: IndexKeyFile = new IndexKeyFile(fileName) def set(key: Array[Byte], dataOffset: Long) { if (this.index == null) { @@ -87,4 +87,13 @@ class DailyCounterIndex(_fileName: String) extends IClose { } } + def closeForce() { + DailyCounterIndex.table.synchronized { + DailyCounterIndex.table.remove(this.fileName); + FileUtil.close(this.index); + this.index = null; + this.refrence = 0; + } + } + } \ No newline at end of file diff --git a/scouter.server/src/scouter/server/netio/service/handle/VisitorService.scala b/scouter.server/src/scouter/server/netio/service/handle/VisitorService.scala index 37cc940f5..5796688f9 100644 --- a/scouter.server/src/scouter/server/netio/service/handle/VisitorService.scala +++ b/scouter.server/src/scouter/server/netio/service/handle/VisitorService.scala @@ -25,6 +25,9 @@ import scouter.net.TcpFlag import scouter.server.db.VisitorDB import scouter.server.netio.service.anotation.ServiceHandler import scouter.net.RequestCmd +import scouter.util.DateUtil +import scouter.lang.pack.MapPack +import scouter.server.db.VisitorHourlyDB class VisitorService { @@ -65,4 +68,53 @@ class VisitorService { dout.writeByte(TcpFlag.HasNEXT); dout.writeValue(new DecimalValue(value)); } + + @ServiceHandler(RequestCmd.VISITOR_LOADDATE_GROUP) + def visitorLoaddateGroup(din: DataInputX, dout: DataOutputX, login: Boolean) { + val m = din.readMapPack(); + val objHashLv = m.getList("objHash"); + val startDate = m.getText("startDate"); + val endDate = m.getText("endDate"); + var time = DateUtil.yyyymmdd(startDate) + var etime = DateUtil.yyyymmdd(endDate) + val resultPack = new MapPack() + while (time <= etime) { + var date = DateUtil.yyyymmdd(time) + var value = VisitorDB.getMergedVisitorObject(date, objHashLv) + resultPack.put("date", date) + resultPack.put("value", value) + dout.writeByte(TcpFlag.HasNEXT); + dout.writePack(resultPack); + time = time + DateUtil.MILLIS_PER_DAY + } + } + + @ServiceHandler(RequestCmd.VISITOR_LOADHOUR_GROUP) + def visitorLoadhourGroup(din: DataInputX, dout: DataOutputX, login: Boolean) { + val m = din.readMapPack(); + val objHashLv = m.getList("objHash"); + val stime = m.getLong("stime"); + val etime = m.getLong("etime"); + val resultPack = new MapPack() + var timeLv = resultPack.newList("time") + var valueLv = resultPack.newList("value") + var time = stime + var date = DateUtil.yyyymmdd(time) + while (time <= etime) { + var dt = DateUtil.yyyymmdd(time) + if (date != dt) { + date = dt + dout.writeByte(TcpFlag.HasNEXT); + dout.writePack(resultPack); + timeLv = resultPack.newList("time") + valueLv = resultPack.newList("value") + } + var value = VisitorHourlyDB.getMergedVisitorObject(date, time, objHashLv) + timeLv.add(time) + valueLv.add(value) + time = time + DateUtil.MILLIS_PER_HOUR + } + dout.writeByte(TcpFlag.HasNEXT); + dout.writePack(resultPack); + } } \ No newline at end of file