Add log websocket (#3859)

This adds a websocket connection for receiving logs.

Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
J-N-K
2023-11-22 21:35:35 +01:00
committed by GitHub
parent 7af02598ef
commit 8a6e7432cd
12 changed files with 272 additions and 7 deletions
@@ -30,6 +30,7 @@ import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.openhab.core.auth.AuthenticationException;
import org.openhab.core.auth.Role;
import org.openhab.core.io.rest.auth.AuthFilter;
import org.openhab.core.io.websocket.event.EventWebSocketAdapter;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.websocket;
package org.openhab.core.io.websocket.event;
import java.util.Objects;
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.websocket;
package org.openhab.core.io.websocket.event;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.websocket;
package org.openhab.core.io.websocket.event;
import java.io.IOException;
import java.lang.reflect.Type;
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.websocket;
package org.openhab.core.io.websocket.event;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
@@ -21,6 +21,7 @@ import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.events.EventSubscriber;
import org.openhab.core.io.websocket.WebSocketAdapter;
import org.openhab.core.items.ItemRegistry;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.websocket;
package org.openhab.core.io.websocket.event;
import java.util.List;
import java.util.regex.Matcher;
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.websocket.log;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.websocket.event.EventDTO;
import org.osgi.service.log.LogLevel;
/**
* The {@link EventDTO} is used for serialization and deserialization of events
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class LogDTO {
public @Nullable String loggerName;
public @Nullable LogLevel level;
public @Nullable Date timestamp;
public long unixtime;
public @Nullable String message;
}
@@ -0,0 +1,148 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.websocket.log;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.osgi.service.log.LogEntry;
import org.osgi.service.log.LogListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
/**
* The {@link LogWebSocket} is the WebSocket implementation for logs
*
* @author Jan N. Klug - Initial contribution
*/
@WebSocket
@NonNullByDefault
@SuppressWarnings("unused")
public class LogWebSocket implements LogListener {
@SuppressWarnings("unchecked")
private static final TypeToken<List<String>> STRING_LIST_TYPE = (TypeToken<List<String>>) TypeToken
.getParameterized(List.class, String.class);
private final Logger logger = LoggerFactory.getLogger(LogWebSocket.class);
private final LogWebSocketAdapter wsAdapter;
private final Gson gson;
private @Nullable Session session;
private @Nullable RemoteEndpoint remoteEndpoint;
private String remoteIdentifier = "<unknown>";
private List<Pattern> loggerPatterns = List.of();
public LogWebSocket(Gson gson, LogWebSocketAdapter wsAdapter) {
this.wsAdapter = wsAdapter;
this.gson = gson;
}
@OnWebSocketClose
public void onClose(int statusCode, String reason) {
this.wsAdapter.unregisterListener(this);
remoteIdentifier = "<unknown>";
this.session = null;
this.remoteEndpoint = null;
}
@OnWebSocketConnect
public void onConnect(Session session) {
this.session = session;
RemoteEndpoint remoteEndpoint = session.getRemote();
this.remoteEndpoint = remoteEndpoint;
this.remoteIdentifier = remoteEndpoint.getInetSocketAddress().toString();
this.wsAdapter.registerListener(this);
}
@OnWebSocketMessage
public void onText(String message) {
RemoteEndpoint remoteEndpoint = this.remoteEndpoint;
if (session == null || remoteEndpoint == null) {
// no connection or no remote endpoint , do nothing this is possible due to async behavior
return;
}
try {
loggerPatterns = gson.fromJson(message, STRING_LIST_TYPE).stream().map(Pattern::compile).toList();
} catch (JsonParseException e) {
logger.warn("Failed to parse '{}' to a list of subscribed loggers", message);
}
}
@OnWebSocketError
public void onError(Session session, @Nullable Throwable error) {
if (session != null) {
session.close();
}
String message = error == null ? "<null>" : Objects.requireNonNullElse(error.getMessage(), "<null>");
logger.info("WebSocket error: {}", message);
onClose(StatusCode.NO_CODE, message);
}
private synchronized void sendMessage(String message) throws IOException {
RemoteEndpoint remoteEndpoint = this.remoteEndpoint;
if (remoteEndpoint == null) {
logger.warn("Could not determine remote endpoint, failed to send '{}'.", message);
return;
}
remoteEndpoint.sendString(message);
}
@Override
public void logged(@NonNullByDefault({}) LogEntry logEntry) {
if (!loggerPatterns.isEmpty() && loggerPatterns.stream().noneMatch(logMatch(logEntry))) {
return;
}
try {
LogDTO logDTO = map(logEntry);
sendMessage(gson.toJson(logDTO));
} catch (IOException e) {
logger.debug("Failed to send log {} to {}: {}", logEntry, remoteIdentifier, e.getMessage());
}
}
private static Predicate<Pattern> logMatch(LogEntry logEntry) {
return pattern -> pattern.matcher(logEntry.getLoggerName()).matches();
}
private static LogDTO map(LogEntry logEntry) {
LogDTO logDTO = new LogDTO();
logDTO.loggerName = logEntry.getLoggerName();
logDTO.level = logEntry.getLogLevel();
logDTO.unixtime = logEntry.getTime();
logDTO.timestamp = new Date(logEntry.getTime());
logDTO.message = logEntry.getMessage();
return logDTO;
}
}
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.websocket.log;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.openhab.core.io.websocket.WebSocketAdapter;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.log.LogReaderService;
import com.google.gson.Gson;
/**
* The {@link LogWebSocketAdapter} allows subscription to log events over WebSocket
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = { WebSocketAdapter.class })
public class LogWebSocketAdapter implements WebSocketAdapter {
public static final String ADAPTER_ID = "logs";
private final Gson gson = new Gson();
private final Set<LogWebSocket> webSockets = new CopyOnWriteArraySet<>();
private final LogReaderService logReaderService;
@Activate
public LogWebSocketAdapter(@Reference LogReaderService logReaderService) {
this.logReaderService = logReaderService;
}
@Deactivate
public void deactivate() {
webSockets.forEach(logReaderService::removeLogListener);
}
public void registerListener(LogWebSocket eventWebSocket) {
webSockets.add(eventWebSocket);
logReaderService.addLogListener(eventWebSocket);
}
public void unregisterListener(LogWebSocket eventWebSocket) {
logReaderService.removeLogListener(eventWebSocket);
webSockets.remove(eventWebSocket);
}
@Override
public String getId() {
return ADAPTER_ID;
}
@Override
public Object createWebSocket(ServletUpgradeRequest servletUpgradeRequest,
ServletUpgradeResponse servletUpgradeResponse) {
return new LogWebSocket(gson, LogWebSocketAdapter.this);
}
}
@@ -43,6 +43,7 @@ import org.mockito.quality.Strictness;
import org.openhab.core.auth.AuthenticationException;
import org.openhab.core.io.rest.auth.AnonymousUserSecurityContext;
import org.openhab.core.io.rest.auth.AuthFilter;
import org.openhab.core.io.websocket.event.EventWebSocket;
import org.osgi.service.http.NamespaceException;
/**
@@ -18,8 +18,8 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.openhab.core.io.websocket.EventWebSocket.WEBSOCKET_EVENT_TYPE;
import static org.openhab.core.io.websocket.EventWebSocket.WEBSOCKET_TOPIC_PREFIX;
import static org.openhab.core.io.websocket.event.EventWebSocket.WEBSOCKET_EVENT_TYPE;
import static org.openhab.core.io.websocket.event.EventWebSocket.WEBSOCKET_TOPIC_PREFIX;
import java.io.IOException;
import java.net.InetSocketAddress;
@@ -39,6 +39,10 @@ import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.io.websocket.event.EventDTO;
import org.openhab.core.io.websocket.event.EventWebSocket;
import org.openhab.core.io.websocket.event.EventWebSocketAdapter;
import org.openhab.core.io.websocket.event.ItemEventUtility;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.items.events.ItemEventFactory;
@@ -27,6 +27,9 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.core.events.Event;
import org.openhab.core.io.websocket.event.EventDTO;
import org.openhab.core.io.websocket.event.EventProcessingException;
import org.openhab.core.io.websocket.event.ItemEventUtility;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.items.events.ItemEvent;