Skip to content

Commit

Permalink
added: Real User Monitoring (RUM) if parameter rum-enabled=true
Browse files Browse the repository at this point in the history
  • Loading branch information
evernat committed Apr 19, 2017
1 parent 7c96614 commit b85652a
Show file tree
Hide file tree
Showing 18 changed files with 1,640 additions and 9 deletions.
10 changes: 10 additions & 0 deletions javamelody-core/src/main/java/net/bull/javamelody/Counter.java
Expand Up @@ -557,6 +557,16 @@ void addRequestForSystemError(String requestName, long duration, long cpuTime,
}
}

void addRumHit(String requestName, long networkTime, long domProcessing, long pageRendering) {
final String aggregateRequestName = getAggregateRequestName(requestName);
final CounterRequest request = requests.get(aggregateRequestName);
if (request != null) {
synchronized (request) {
request.addRumHit(networkTime, domProcessing, pageRendering);
}
}
}

/**
* Retourne true si ce counter est un counter d'error
* (c'est-à-dire si son nom est "error", "log" ou "job")
Expand Down
Expand Up @@ -56,6 +56,8 @@ class CounterRequest implements Cloneable, Serializable {
@SuppressWarnings("all")
private Map<String, Long> childRequestsExecutionsByRequestId;

private CounterRequestRumData rumData;

/**
* Interface du contexte d'une requête en cours.
*/
Expand Down Expand Up @@ -257,6 +259,10 @@ String getStackTrace() {
return stackTrace;
}

CounterRequestRumData getRumData() {
return rumData;
}

void addHit(long duration, long cpuTime, boolean systemError, String systemErrorStackTrace,
int responseSize) {
hits++;
Expand Down Expand Up @@ -326,6 +332,13 @@ void addHits(CounterRequest request) {
}
addChildRequests(request.childRequestsExecutionsByRequestId);
}
if (request.rumData != null) {
if (rumData != null) {
rumData.addHits(request.rumData);
} else {
rumData = request.rumData.clone();
}
}
}

void removeHits(CounterRequest request) {
Expand All @@ -351,6 +364,9 @@ void removeHits(CounterRequest request) {

removeChildHits(request);
}
if (rumData != null && request.rumData != null) {
rumData.removeHits(request.rumData);
}
}

private void removeChildHits(CounterRequest request) {
Expand All @@ -376,6 +392,13 @@ private void removeChildHits(CounterRequest request) {
}
}

void addRumHit(long networkTime, long domProcessing, long pageRendering) {
if (rumData == null) {
rumData = new CounterRequestRumData();
}
rumData.addHit(networkTime, domProcessing, pageRendering);
}

/** {@inheritDoc} */
@Override
public CounterRequest clone() { // NOPMD
Expand All @@ -385,6 +408,9 @@ public CounterRequest clone() { // NOPMD
// getChildRequestsExecutionsByRequestId fait déjà un clone de la map
clone.childRequestsExecutionsByRequestId = getChildRequestsExecutionsByRequestId();
}
if (rumData != null) {
clone.rumData = rumData.clone();
}
return clone;
} catch (final CloneNotSupportedException e) {
// ne peut arriver puisque CounterRequest implémente Cloneable
Expand Down
@@ -0,0 +1,110 @@
/*
* Copyright 2008-2017 by Emeric Vernat
*
* This file is part of Java Melody.
*
* 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 net.bull.javamelody;

import java.io.Serializable;

/**
* Données Real User Monitoring (RUM) d'une requête http.
* @author Emeric Vernat
*/
class CounterRequestRumData implements Serializable, Cloneable {
private static final long serialVersionUID = 745110095604593659L;

// au-delà de 5 minutes, on considère une valeur RUM comme aberrante et à ignorer
private static final long ABERRANT_VALUE = 5 * 60 * 1000;

private long hits;
private long networkTimeSum;
private long domProcessingSum;
private long pageRenderingSum;

long getHits() {
return hits;
}

int getNetworkTimeMean() {
if (hits > 0) {
return (int) (networkTimeSum / hits);
}
return -1;
}

int getDomProcessingMean() {
if (hits > 0) {
return (int) (domProcessingSum / hits);
}
return -1;
}

int getPageRenderingMean() {
if (hits > 0) {
return (int) (pageRenderingSum / hits);
}
return -1;
}

void addHit(long networkTime, long domProcessing, long pageRendering) {
if (networkTime < 0 || networkTime > ABERRANT_VALUE || domProcessing < 0
|| domProcessing > ABERRANT_VALUE || pageRendering < 0
|| pageRendering > ABERRANT_VALUE) {
// aberrant value, we ignore it
return;
}

networkTimeSum += networkTime;
domProcessingSum += domProcessing;
pageRenderingSum += pageRendering;
hits++;
}

void addHits(CounterRequestRumData rumData) {
if (rumData.hits != 0) {
hits += rumData.hits;
networkTimeSum += rumData.networkTimeSum;
domProcessingSum += rumData.domProcessingSum;
pageRenderingSum += rumData.pageRenderingSum;
}
}

void removeHits(CounterRequestRumData rumData) {
if (rumData.hits != 0) {
hits -= rumData.hits;
networkTimeSum -= rumData.networkTimeSum;
domProcessingSum -= rumData.domProcessingSum;
pageRenderingSum -= rumData.pageRenderingSum;
}
}

/** {@inheritDoc} */
@Override
public CounterRequestRumData clone() { // NOPMD
try {
return (CounterRequestRumData) super.clone();
} catch (final CloneNotSupportedException e) {
// ne peut arriver puisque CounterRequest implémente Cloneable
throw new IllegalStateException(e);
}
}

/** {@inheritDoc} */
@Override
public String toString() {
return getClass().getSimpleName() + "[hits=" + hits + ']';
}
}
Expand Up @@ -20,6 +20,7 @@
import java.io.IOException;
import java.io.Writer;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -113,6 +114,9 @@ void writeRequestAndGraphDetail(Collector collector, CollectorServer collectorSe
requestsById = mapAllRequestsById();
final CounterRequest request = requestsById.get(graphName);
if (request != null) {
if (request.getRumData() != null && request.getRumData().getHits() > 0) {
writeRequestRumData(request);
}
writeRequest(request);

if (JdbcWrapper.SINGLETON.getSqlCounter().isRequestIdFromThisCounter(graphName)
Expand Down Expand Up @@ -270,6 +274,42 @@ private boolean getUsagesDisplayed(List<CounterRequest> requests) {
return false;
}

private void writeRequestRumData(CounterRequest request) throws IOException {
final CounterRequestRumData rumData = request.getRumData();
final DecimalFormat percentUsFormat = new DecimalFormat("0.00",
DecimalFormatSymbols.getInstance(Locale.US));
final DecimalFormat percentLocaleFormat = I18N.createPercentFormat();
final int networkTimeMean = rumData.getNetworkTimeMean();
final int serverMean = request.getMean();
final int domProcessingMean = rumData.getDomProcessingMean();
final int pageRenderingMean = rumData.getPageRenderingMean();
final int total = networkTimeMean + serverMean + domProcessingMean + pageRenderingMean;
final double networkPercent = 100d * networkTimeMean / total;
final double serverPercent = 100d * serverMean / total;
final double domProcessingPercent = 100d * domProcessingMean / total;
final double pageRenderingPercent = 100d * pageRenderingMean / total;
writeln("<br/><table class='rumData' summary=''><tr>");
writeln("<td class='rumDataNetwork tooltip' style='width:"
+ percentUsFormat.format(networkPercent) + "%'><em>#Network#: "
+ integerFormat.format(networkTimeMean) + " ms ("
+ percentLocaleFormat.format(networkPercent) + "%)</em>#Network#</td>");
writeln("<td class='rumDataServer tooltip' style='width:"
+ percentUsFormat.format(serverPercent) + "%'><em>#Server#: "
+ integerFormat.format(serverMean) + " ms ("
+ percentLocaleFormat.format(serverPercent) + "%)</em>#Server#</td>");
writeln("<td class='rumDataDomProcessing tooltip' style='width:"
+ percentUsFormat.format(domProcessingPercent) + "%'><em>#DOM_processing#:"
+ integerFormat.format(domProcessingMean) + " ms ("
+ percentLocaleFormat.format(domProcessingPercent)
+ "%)</em>#DOM_processing#</td>");
writeln("<td class='rumDataPageRendering tooltip' style='width:"
+ percentUsFormat.format(pageRenderingPercent) + "%'><em>#Page_rendering#:"
+ integerFormat.format(pageRenderingMean) + " ms ("
+ percentLocaleFormat.format(pageRenderingPercent)
+ "%)</em>#Page_rendering#</td>");
writeln("</tr></table>");
}

private void writeRequest(CounterRequest request) throws IOException {
final Map<String, Long> childRequests = request.getChildRequestsExecutionsByRequestId();
writeln(" <br/>");
Expand Down
@@ -0,0 +1,130 @@
/*
* Copyright 2008-2017 by Emeric Vernat
*
* This file is part of Java Melody.
*
* 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 net.bull.javamelody;

import java.io.IOException;

import javax.servlet.http.HttpServletResponse;

/**
* Implémentation de ServletOutputStream qui fonctionne avec le HtmlInjectorServletResponseWrapper.
* @author Emeric Vernat
*/
class HtmlInjectorResponseStream extends FilterServletOutputStream {
private final HttpServletResponse response;
private final HtmlToInject htmlToInject;
private final byte[] beforeTag;
private boolean injectionCanceled;

interface HtmlToInject {
/**
* @return Html content to inject.
*/
String getContent();

/**
* @return Portion of html to inject content before.
*/
String getBeforeTag();
}

/**
* Construit un servlet output stream associé avec la réponse spécifiée.
* @param response HttpServletResponse
* @param htmlToInject HtmlToInject
* @throws IOException Erreur d'entrée/sortie
*/
HtmlInjectorResponseStream(HttpServletResponse response, HtmlToInject htmlToInject)
throws IOException {
super(response.getOutputStream());
this.response = response;
this.htmlToInject = htmlToInject;
// HttpServletResponse.getCharacterEncoding() shouldn't return null according the spec.
// And response.getCharacterEncoding() may not be explicit yet,
// but we suppose that it does not make any difference on the beforeTag.
this.beforeTag = htmlToInject.getBeforeTag().getBytes(response.getCharacterEncoding());
}

void cancelInjection() {
injectionCanceled = true;
}

// not worth it
// /** {@inheritDoc} */
// @Override
// public void write(int i) throws IOException {
// super.write(i);
// }

/** {@inheritDoc} */
@Override
public void write(byte[] bytes) throws IOException {
write(bytes, 0, bytes.length);
}

/** {@inheritDoc} */
@Override
public void write(byte[] bytes, int off, int len) throws IOException {
// if httpResponse.setContentType(x) has been called with !x.contains("text/html"),
// then no need to continue scanning for the beforeTag
if (injectionCanceled) {
super.write(bytes, off, len);
} else {
final int index = indexOf(bytes, beforeTag, off, len);
if (index == -1) {
// beforeTag not found yet
super.write(bytes, off, len);
} else {
// beforeTag found: inject content.
super.write(bytes, off, index);
final String content = htmlToInject.getContent();
// HttpServletResponse.getCharacterEncoding() shouldn't return null according the spec
super.write(content.getBytes(response.getCharacterEncoding()));
super.write(bytes, off + index, len - index);
}
}
}

private static int indexOf(byte[] sourceBytes, byte[] targetBytes, int sourceOffset,
int sourceLength) {
final byte first = targetBytes[0];
final int max = sourceOffset + (sourceLength - targetBytes.length);

for (int i = sourceOffset; i <= max; i++) {
// Look for first byte
while (i <= max && sourceBytes[i] != first) {
i++;
}

if (i <= max) {
// Found first byte, now look at the rest of sourceBytes
int j = i + 1;
final int end = j + targetBytes.length - 1;
for (int k = 1; j < end && sourceBytes[j] == targetBytes[k]; k++) {
j++;
}

if (j == end) {
// Found whole bytes
return i - sourceOffset;
}
}
}
return -1;
}
}

0 comments on commit b85652a

Please sign in to comment.