blob: 5730e11b1a180dc1390ab9fd766ece5b910008d8 [file] [log] [blame]
/******************************************************************************
* Copyright (c) 2016-2019 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc;
import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler;
import org.eclipse.lsp4j.jsonrpc.json.StreamMessageConsumer;
import org.eclipse.lsp4j.jsonrpc.messages.Message;
import org.eclipse.lsp4j.jsonrpc.messages.NotificationMessage;
import org.eclipse.lsp4j.jsonrpc.messages.RequestMessage;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage;
import java.io.PrintWriter;
import java.time.Clock;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
/**
* A {@link MessageConsumer} that outputs logs in a format that can be parsed by the LSP Inspector.
* https://microsoft.github.io/language-server-protocol/inspector/
*/
public class TracingMessageConsumer implements MessageConsumer {
private static final Logger LOG = Logger.getLogger(TracingMessageConsumer.class.getName());
private final MessageConsumer messageConsumer;
private final Map<String, RequestMetadata> sentRequests;
private final Map<String, RequestMetadata> receivedRequests;
private final PrintWriter printWriter;
private final Clock clock;
private final DateTimeFormatter dateTimeFormatter;
/**
* @param messageConsumer The {@link MessageConsumer} to wrap.
* @param sentRequests A map that keeps track of pending sent request data.
* @param receivedRequests A map that keeps track of pending received request data.
* @param printWriter Where to write the log to.
* @param clock The clock that is used to calculate timestamps and durations.
*/
public TracingMessageConsumer(
MessageConsumer messageConsumer,
Map<String, RequestMetadata> sentRequests,
Map<String, RequestMetadata> receivedRequests,
PrintWriter printWriter,
Clock clock) {
this(messageConsumer, sentRequests, receivedRequests, printWriter, clock, null);
}
/**
* @param messageConsumer The {@link MessageConsumer} to wrap.
* @param sentRequests A map that keeps track of pending sent request data.
* @param receivedRequests A map that keeps track of pending received request data.
* @param printWriter Where to write the log to.
* @param clock The clock that is used to calculate timestamps and durations.
* @param locale THe Locale to format the timestamps and durations, or <code>null</code> to use default locale.
*/
public TracingMessageConsumer(
MessageConsumer messageConsumer,
Map<String, RequestMetadata> sentRequests,
Map<String, RequestMetadata> receivedRequests,
PrintWriter printWriter,
Clock clock,
Locale locale) {
this.messageConsumer = Objects.requireNonNull(messageConsumer);
this.sentRequests = Objects.requireNonNull(sentRequests);
this.receivedRequests = Objects.requireNonNull(receivedRequests);
this.printWriter = Objects.requireNonNull(printWriter);
this.clock = Objects.requireNonNull(clock);
if (locale == null) {
this.dateTimeFormatter = DateTimeFormatter.ofPattern("KK:mm:ss a").withZone(clock.getZone());
} else {
this.dateTimeFormatter = DateTimeFormatter.ofPattern("KK:mm:ss a", locale).withZone(clock.getZone());
}
}
/**
* Constructs a log string for a given {@link Message}. The type of the {@link MessageConsumer}
* determines if we're sending or receiving a message. The type of the @{link Message} determines
* if it is a request, response, or notification.
*/
@Override
public void consume(Message message) throws MessageIssueException, JsonRpcException {
final Instant now = clock.instant();
final String date = dateTimeFormatter.format(now);
final String logString;
if (messageConsumer instanceof StreamMessageConsumer) {
logString = consumeMessageSending(message, now, date);
} else if (messageConsumer instanceof RemoteEndpoint) {
logString = consumeMessageReceiving(message, now, date);
} else {
LOG.log(WARNING, String.format("Unknown MessageConsumer type: %s", messageConsumer));
logString = null;
}
if (logString != null) {
printWriter.print(logString);
printWriter.flush();
}
messageConsumer.consume(message);
}
private String consumeMessageSending(Message message, Instant now, String date) {
if (message instanceof RequestMessage) {
RequestMessage requestMessage = (RequestMessage) message;
String id = requestMessage.getId();
String method = requestMessage.getMethod();
RequestMetadata requestMetadata = new RequestMetadata(method, now);
sentRequests.put(id, requestMetadata);
Object params = requestMessage.getParams();
String paramsJson = MessageJsonHandler.toString(params);
String format = "[Trace - %s] Sending request '%s - (%s)'\nParams: %s\n\n\n";
return String.format(format, date, method, id, paramsJson);
} else if (message instanceof ResponseMessage) {
ResponseMessage responseMessage = (ResponseMessage) message;
String id = responseMessage.getId();
RequestMetadata requestMetadata = receivedRequests.remove(id);
if (requestMetadata == null) {
LOG.log(WARNING, String.format("Unmatched response message: %s", message));
return null;
}
String method = requestMetadata.method;
long latencyMillis = now.toEpochMilli() - requestMetadata.start.toEpochMilli();
Object result = responseMessage.getResult();
String resultJson = MessageJsonHandler.toString(result);
String format =
"[Trace - %s] Sending response '%s - (%s)'. Processing request took %sms\nResult: %s\n\n\n";
return String.format(format, date, method, id, latencyMillis, resultJson);
} else if (message instanceof NotificationMessage) {
NotificationMessage notificationMessage = (NotificationMessage) message;
String method = notificationMessage.getMethod();
Object params = notificationMessage.getParams();
String paramsJson = MessageJsonHandler.toString(params);
String format = "[Trace - %s] Sending notification '%s'\nParams: %s\n\n\n";
return String.format(format, date, method, paramsJson);
} else {
LOG.log(WARNING, String.format("Unknown message type: %s", message));
return null;
}
}
private String consumeMessageReceiving(Message message, Instant now, String date) {
if (message instanceof RequestMessage) {
RequestMessage requestMessage = (RequestMessage) message;
String method = requestMessage.getMethod();
String id = requestMessage.getId();
RequestMetadata requestMetadata = new RequestMetadata(method, now);
receivedRequests.put(id, requestMetadata);
Object params = requestMessage.getParams();
String paramsJson = MessageJsonHandler.toString(params);
String format = "[Trace - %s] Received request '%s - (%s)'\nParams: %s\n\n\n";
return String.format(format, date, method, id, paramsJson);
} else if (message instanceof ResponseMessage) {
ResponseMessage responseMessage = (ResponseMessage) message;
String id = responseMessage.getId();
RequestMetadata requestMetadata = sentRequests.remove(id);
if (requestMetadata == null) {
LOG.log(WARNING, String.format("Unmatched response message: %s", message));
return null;
}
String method = requestMetadata.method;
long latencyMillis = now.toEpochMilli() - requestMetadata.start.toEpochMilli();
Object result = responseMessage.getResult();
String resultJson = MessageJsonHandler.toString(result);
Object error = responseMessage.getError();
String errorJson = MessageJsonHandler.toString(error);
String format = "[Trace - %s] Received response '%s - (%s)' in %sms\nResult: %s\nError: %s\n\n\n";
return String.format(format, date, method, id, latencyMillis, resultJson, errorJson);
} else if (message instanceof NotificationMessage) {
NotificationMessage notificationMessage = (NotificationMessage) message;
String method = notificationMessage.getMethod();
Object params = notificationMessage.getParams();
String paramsJson = MessageJsonHandler.toString(params);
String format = "[Trace - %s] Received notification '%s'\nParams: %s\n\n\n";
return String.format(format, date, method, paramsJson);
} else {
LOG.log(WARNING, String.format("Unknown message type: %s", message));
return null;
}
}
/** Data class for holding pending request metadata. */
public static class RequestMetadata {
final String method;
final Instant start;
public RequestMetadata(String method, Instant start) {
this.method = method;
this.start = start;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RequestMetadata that = (RequestMetadata) o;
return Objects.equals(method, that.method) && Objects.equals(start, that.start);
}
@Override
public int hashCode() {
return Objects.hash(method, start);
}
}
}