/*******************************************************************************
* Copyright (c) 2012-2015 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.analytics.logger;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashSet;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @author Anatoliy Bazko
*/
@Singleton
public class EventLogger {
private static final Logger LOG = LoggerFactory.getLogger(EventLogger.class);
public static final String DASHBOARD_SOURCE = "com.codenvy.dashboard";
public static final String EVENT_PARAM = "EVENT";
public static final String WS_PARAM = "WS";
public static final String USER_PARAM = "USER";
public static final String SOURCE_PARAM = "SOURCE";
public static final String ACTION_PARAM = "ACTION";
public static final String PROJECT_NAME_PARAM = "PROJECT";
public static final String PROJECT_TYPE_PARAM = "TYPE";
public static final String PARAMETERS_PARAM = "PARAMETERS";
public static final String IDE_USAGE = "ide-usage";
public static final String DASHBOARD_USAGE = "dashboard-usage";
public static final String USER_INVITE = "user-invite";
public static final String SESSION_USAGE = "session-usage";
public static final String SESSION_FACTORY_USAGE = "session-factory-usage";
private static final int MAX_EXTENDED_PARAMS_NUMBER = 3;
private static final int RESERVED_PARAMS_NUMBER = 6;
private static final int MAX_PARAM_NAME_LENGTH = 20;
private static final int MAX_PARAM_VALUE_LENGTH = 100;
private static final int QUEUE_MAX_CAPACITY = 10000;
private static final Set<String> ALLOWED_EVENTS = new HashSet<String>() {{
add(IDE_USAGE);
add(DASHBOARD_USAGE);
add(USER_INVITE);
add(SESSION_USAGE);
add(SESSION_FACTORY_USAGE);
}};
private final Thread logThread;
private final Queue<String> queue;
/**
* Stores the number of ignored events due to maximum queue capacity
*/
private long ignoredEvents;
public EventLogger() {
this.queue = new LinkedBlockingQueue<>(QUEUE_MAX_CAPACITY);
this.ignoredEvents = 0;
logThread = new LogThread();
logThread.setDaemon(true);
}
@PostConstruct
public void init() {
logThread.start();
}
@PreDestroy
public void destroy() {
logThread.interrupt();
}
public void log(String event, Map<String, String> parameters) throws UnsupportedEncodingException {
if (event != null && ALLOWED_EVENTS.contains(event)) {
if (event.equals(DASHBOARD_USAGE)) {
parameters.put(EventLogger.SOURCE_PARAM, EventLogger.DASHBOARD_SOURCE);
}
validate(parameters);
String message = createMessage(event, parameters);
if (!offerEvent(message)) {
if (ignoredEvents++ % 1000 == 0) {
LOG.warn("Ignored " + ignoredEvents + " events due to maximum queue capacity");
}
}
}
}
protected boolean offerEvent(String message) {
return queue.offer(message);
}
private String createMessage(String event, Map<String, String> parameters) throws UnsupportedEncodingException {
StringBuilder message = new StringBuilder();
addParam(message, EVENT_PARAM, event);
addParam(message, WS_PARAM, parameters);
addParam(message, USER_PARAM, parameters);
addParam(message, PROJECT_NAME_PARAM, parameters);
addParam(message, PROJECT_TYPE_PARAM, parameters);
addParam(message, SOURCE_PARAM, parameters);
addParam(message, ACTION_PARAM, parameters);
addParam(message, PARAMETERS_PARAM, getParametersAsString(parameters));
return message.toString();
}
private String getParametersAsString(Map<String, String> parameters) throws UnsupportedEncodingException {
StringBuilder builder = new StringBuilder();
for (Map.Entry<String, String> entry : parameters.entrySet()) {
if (builder.length() > 0) {
builder.append(',');
}
builder.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
builder.append('=');
builder.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
}
return builder.toString();
}
private void addParam(StringBuilder message, String param, Map<String, String> parameters) {
if (parameters.containsKey(param)) {
addParam(message, param, parameters.remove(param));
}
}
private void addParam(StringBuilder message, String param, String value) {
if (message.length() > 0) {
message.append(' ');
}
message.append(param);
message.append('#');
message.append(value);
message.append('#');
}
private void validate(Map<String, String> additionalParams) throws IllegalArgumentException {
if (additionalParams.size() > MAX_EXTENDED_PARAMS_NUMBER + RESERVED_PARAMS_NUMBER) {
throw new IllegalArgumentException("The number of parameters exceeded the limit in " +
MAX_EXTENDED_PARAMS_NUMBER);
}
for (Map.Entry<String, String> entry : additionalParams.entrySet()) {
String param = entry.getKey();
String value = entry.getValue();
if (param.length() > MAX_PARAM_NAME_LENGTH) {
throw new IllegalArgumentException(
"The length of parameter name " + param + " exceeded the length in " + MAX_PARAM_NAME_LENGTH +
" characters");
} else if (value.length() > MAX_PARAM_VALUE_LENGTH) {
throw new IllegalArgumentException(
"The length of parameter value " + value + " exceeded the length in " + MAX_PARAM_VALUE_LENGTH +
" characters");
}
}
}
/**
* Is responsible for logging events.
* Rate-limit is 50 messages per second.
*/
private class LogThread extends Thread {
private LogThread() {
super("Analytics Event Logger");
}
@Override
public void run() {
LOG.info(getName() + " thread is started, queue is initialized for " + QUEUE_MAX_CAPACITY + " messages");
while (!isInterrupted()) {
String message = queue.poll();
try {
if (message != null) {
LOG.info(message);
sleep(20);
} else {
sleep(1000);
}
} catch (InterruptedException e) {
break;
}
}
LOG.info(getName() + " thread is stopped");
}
}
}