--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>org.simantics.auditlogging</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.ManifestBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.SchemaBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.pde.PluginNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
--- /dev/null
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.8
--- /dev/null
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Simantics Audit Logging
+Bundle-SymbolicName: org.simantics.auditlogging
+Bundle-Version: 1.0.0.qualifier
+Bundle-Activator: org.simantics.audit.Activator
+Require-Bundle: org.eclipse.core.runtime,
+ com.fasterxml.jackson.core.jackson-databind,
+ com.fasterxml.jackson.core.jackson-core,
+ org.slf4j.api,
+ javax.servlet-api,
+ javax.ws.rs-api,
+ org.eclipse.jetty.server,
+ org.eclipse.jetty.servlet,
+ org.eclipse.jetty.util,
+ org.glassfish.jersey.core.jersey-server,
+ org.glassfish.jersey.media.jersey-media-json-jackson,
+ org.glassfish.jersey.containers.jersey-container-servlet-core,
+ org.glassfish.jersey.core.jersey-client,
+ org.glassfish.jersey.core.jersey-common
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Bundle-ActivationPolicy: lazy
--- /dev/null
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+ .,\
+ scl/
--- /dev/null
+import "Map" as Map
+
+importJava "org.simantics.audit.client.AuditLoggingClient" where
+ @JavaName sendLog
+ sendLogM :: Map.T String a -> <Proc> ()
+ sendLog :: [a] -> <Proc> ()
+
+ @JavaName sendError
+ sendErrorM :: Map.T String a -> <Proc> ()
+ sendError :: [a] -> <Proc> ()
+
+ @JavaName sendTrace
+ sendTraceM :: Map.T String a -> <Proc> ()
+ sendTrace :: [a] -> <Proc> ()
+
\ No newline at end of file
--- /dev/null
+import "Map" as Map
+
+importJava "org.simantics.audit.AuditLogging" where
+ register :: String -> <Proc> String
+ log :: String -> Map.T String a -> <Proc, Exception> ()
+ error :: String -> Map.T String a -> <Proc, Exception> ()
+ trace :: String -> Map.T String a -> <Proc, Exception> ()
+ getLogEvents :: String -> String -> String -> String -> <Proc, Exception> [String]
+
+importJava "org.simantics.audit.server.AuditLoggingServer" where
+ start :: String -> Integer -> <Proc> ()
\ No newline at end of file
--- /dev/null
+package org.simantics.audit;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.Platform;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+public class Activator implements BundleActivator {
+
+ private static BundleContext context;
+ private static Path logLocation;
+
+ static BundleContext getContext() {
+ return context;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)
+ */
+ public void start(BundleContext bundleContext) throws Exception {
+ Activator.context = bundleContext;
+ IPath ipath = Platform.getStateLocation(getContext().getBundle());
+ logLocation = Paths.get(ipath.toOSString());
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)
+ */
+ public void stop(BundleContext bundleContext) throws Exception {
+ Activator.context = null;
+ }
+
+ public static Path getLogLocation() {
+ return logLocation;
+ }
+
+}
--- /dev/null
+package org.simantics.audit;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystemException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class AuditLogging {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AuditLogging.class);
+
+ private static ObjectMapper mapper = new ObjectMapper();
+
+ public enum Level {
+ INFO,
+ ERROR,
+ TRACE
+ }
+
+ public static String register(String id) throws AuditLoggingException {
+ try {
+ String entryRoot = id + "_" + UUID.randomUUID().toString();
+ Files.createDirectories(getEntryRoot(entryRoot));
+ return entryRoot;
+ } catch (Exception e) {
+ throw new AuditLoggingException("Could not register service with id " + id, e);
+ }
+ }
+
+ public static List<String> getLogEvents(String uuid, String level, String startDate, String endDate) throws AuditLoggingException {
+ Path entryRoot = getEntryRoot(uuid);
+ try {
+ LocalDate localStartDate = LocalDate.parse(startDate);
+ LocalDate localEndDate = LocalDate.parse(endDate).plusDays(1);
+ List<String> allLines = new ArrayList<>();
+ while (localStartDate.isBefore(localEndDate)) {
+ String fileName = resolveLogFileName(uuid, Level.valueOf(level.toUpperCase()), localStartDate);
+ try {
+ List<String> lines = Files.readAllLines(entryRoot.resolve(fileName));
+ allLines.addAll(lines);
+ } catch (FileSystemException e) {
+ // presumably file not found but lets not throw this cause forward, log here
+ LOGGER.error("Could not read file {}", fileName, e);
+ } finally {
+ localStartDate = localStartDate.plusDays(1);
+ }
+ }
+ return allLines;
+ } catch (Exception e) {
+ throw new AuditLoggingException(e);
+ }
+ }
+
+
+ public static Path getEntryRoot(String uuid) {
+ return Activator.getLogLocation().resolve(uuid);
+ }
+
+ public static Path getLogFile(String uuid, Level level) {
+ Path root = getEntryRoot(uuid);
+ String fileName = resolveLogFileName(uuid, level, LocalDate.now());
+ return root.resolve(fileName);
+ }
+
+ private static String resolveLogFileName(String uuid, Level level, LocalDate date) {
+ return date.toString() + "_" + uuid + "." + level.toString().toLowerCase();
+ }
+
+ public static void log(String uuid, Map<String, Object> json) throws AuditLoggingException {
+ write(uuid, Level.INFO, json);
+ }
+
+ public static void error(String uuid, Map<String, Object> json) throws AuditLoggingException {
+ write(uuid, Level.ERROR, json);
+ }
+
+ public static void trace(String uuid, Map<String, Object> json) throws AuditLoggingException {
+ write(uuid, Level.TRACE, json);
+ }
+
+ private static void write(String uuid, Level level, Map<String, Object> json) throws AuditLoggingException {
+ Map<String, Object> wrappedAuditEvent = wrapAndAddAuditMetadata(uuid, level, json);
+ try {
+ String jsonLine = mapper.writeValueAsString(wrappedAuditEvent);
+ Path logFile = getLogFile(uuid, level);
+ if (!Files.exists(logFile))
+ Files.createFile(logFile);
+ String lineWithNewline = jsonLine + "\n";
+ Files.write(logFile, lineWithNewline.getBytes(StandardCharsets.UTF_8), StandardOpenOption.WRITE, StandardOpenOption.APPEND);
+ } catch (JsonProcessingException e) {
+ throw new AuditLoggingException("Could not serialize input", e);
+ } catch (IOException e) {
+ throw new AuditLoggingException("Could not write line to log", e);
+ }
+ }
+
+ private static final String timestamp = "timestamp";
+
+ private static Map<String, Object> wrapAndAddAuditMetadata(String uuid, Level level, Map<String, Object> original) {
+ Map<String, Object> wrapped = new HashMap<>();
+ long newValue = System.currentTimeMillis();
+ Object possibleExisting = wrapped.put(timestamp, newValue);
+ if (possibleExisting != null) {
+ LOGGER.warn("Replacing existing value {} for key {} - new value is {}", possibleExisting, timestamp, newValue);
+ }
+ possibleExisting = wrapped.put("uuid", uuid);
+ if (possibleExisting != null) {
+ LOGGER.warn("Replacing existing value {} for key {} - new value is {}", possibleExisting, "uuid", uuid);
+ }
+ possibleExisting = wrapped.put("level", level.toString());
+ if (possibleExisting != null) {
+ LOGGER.warn("Replacing existing value {} for key {} - new value is {}", possibleExisting, "level", level.toString());
+ }
+ wrapped.put("original", original);
+ return wrapped;
+ }
+}
--- /dev/null
+package org.simantics.audit;
+
+public class AuditLoggingException extends Exception {
+
+ private static final long serialVersionUID = -5798538535211386651L;
+
+ public AuditLoggingException() {
+ super();
+ }
+
+ public AuditLoggingException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public AuditLoggingException(String message) {
+ super(message);
+ }
+
+ public AuditLoggingException(Throwable cause) {
+ super(cause);
+ }
+
+}
--- /dev/null
+package org.simantics.audit.client;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Collections;
+import java.util.Map;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.simantics.audit.Activator;
+import org.simantics.audit.AuditLoggingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AuditLoggingAPIClient {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AuditLoggingAPIClient.class);
+
+ private Client httpClient;
+ private WebTarget base;
+ private String uuid;
+
+ public AuditLoggingAPIClient(String id, String serverAddress) throws AuditLoggingException {
+ ClientConfig configuration = new ClientConfig();
+ configuration.register(JacksonFeature.class);
+ httpClient = ClientBuilder.newClient(configuration);
+ if (!serverAddress.startsWith("http://")) {
+ serverAddress = "http://" + serverAddress;
+ }
+ base = httpClient.target(serverAddress);
+
+ // see if registered already
+ uuid = possibleUUIDFromFile();
+ if (uuid == null) {
+ // register
+ register(id);
+ }
+ }
+
+ private void register(String id) throws AuditLoggingException {
+ try {
+ Response response = base.path("register").request(MediaType.APPLICATION_JSON_TYPE).post(Entity.json(Collections.singletonMap("id", id)));
+ Map<String, String> payload = response.readEntity(Map.class);
+ String possibleUUID = payload.get("uuid");
+ if (possibleUUID != null && !possibleUUID.isEmpty()) {
+ persistUUID(possibleUUID);
+ } else {
+ LOGGER.warn("Invalid response received from {} for register with response payload {}", base.getUri(), payload);
+ }
+ } catch (Exception e) {
+ throw new AuditLoggingException(e);
+ }
+ }
+
+ private static Path auditLoggingFile() {
+ return Activator.getLogLocation().resolve(".auditlogging");
+ }
+
+ private static String possibleUUIDFromFile() {
+ Path auditLoggingFile = auditLoggingFile();
+ if (Files.exists(auditLoggingFile)) {
+ try {
+ String possibleUUID = new String(Files.readAllBytes(auditLoggingFile));
+ if (possibleUUID != null && !possibleUUID.isEmpty()) {
+ return possibleUUID;
+ } else {
+ LOGGER.warn(".auditlogging file exists but is somehow corrupted");
+ }
+ } catch (IOException e) {
+ LOGGER.error("Could not read .auditlogging file and related information", e);
+ }
+ }
+ return null;
+ }
+
+ private void persistUUID(String possibleUUID) throws IOException {
+ Path auditLoggingFile = auditLoggingFile();
+ Files.write(auditLoggingFile, possibleUUID.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
+ uuid = possibleUUID;
+ }
+
+ public void log(Map<String, Object> message) throws AuditLoggingException {
+ try {
+ Response response = base.path(uuid).path("log").request(MediaType.APPLICATION_JSON_TYPE).post(Entity.json(message));
+ } catch (Exception e) {
+ throw new AuditLoggingException(e);
+ }
+ }
+
+ public void error(Map<String, Object> message) throws AuditLoggingException {
+ try {
+ Response response = base.path(uuid).path("error").request(MediaType.APPLICATION_JSON_TYPE).post(Entity.json(message));
+ } catch (Exception e) {
+ throw new AuditLoggingException(e);
+ }
+ }
+
+ public void trace(Map<String, Object> message) throws AuditLoggingException {
+ try {
+ Response response = base.path(uuid).path("trace").request(MediaType.APPLICATION_JSON_TYPE).post(Entity.json(message));
+ } catch (Exception e) {
+ throw new AuditLoggingException(e);
+ }
+ }
+
+}
--- /dev/null
+package org.simantics.audit.client;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.simantics.audit.AuditLogging;
+import org.simantics.audit.AuditLogging.Level;
+import org.simantics.audit.AuditLoggingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AuditLoggingClient {
+
+ private static final String AUDIT_SERVER_ADDRESS = "org.simantics.audit.serverAddress";
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AuditLoggingClient.class);
+
+ private static AuditLoggingClient INSTANCE;
+
+ private AuditLoggingAPIClient apiClient;
+
+ private AuditLoggingClient() throws AuditLoggingException {
+ // Read config from sysargs
+ System.out.println("asd");
+ String serverAddress = System.getProperty(AUDIT_SERVER_ADDRESS);
+ if (serverAddress != null && !serverAddress.isEmpty()) {
+ apiClient = new AuditLoggingAPIClient("testlog", serverAddress);
+
+ } else {
+ LOGGER.warn("No {} system property defined so client not configured", AUDIT_SERVER_ADDRESS);
+ }
+ }
+
+ private static AuditLoggingClient instance() throws AuditLoggingException {
+ if (INSTANCE == null) {
+ synchronized (AuditLoggingClient.class) {
+ if (INSTANCE == null) {
+ INSTANCE = new AuditLoggingClient();
+ }
+ }
+ }
+ return INSTANCE;
+ }
+
+ public static void sendLog(List<Object> keyValues) throws AuditLoggingException {
+ commit(Level.INFO, toMap(keyValues.toArray()));
+ }
+
+ private static Map<String, Object> toMap(Object... keyValues) {
+ if ((keyValues.length % 2) != 0)
+ throw new IllegalArgumentException("Invalid amount of arguments! " + Arrays.toString(keyValues));
+ Map<String, Object> results = new HashMap<>(keyValues.length / 2);
+ for (int i = 0; i < keyValues.length; i += 2) {
+ Object key = keyValues[i];
+ Object value = keyValues[i + 1];
+ if (!(key instanceof String))
+ throw new IllegalArgumentException("Key with index " + i + " is not String");
+ results.put((String) key, value);
+ }
+ return results;
+ }
+
+ public static void sendLog(Map<String, Object> event) throws AuditLoggingException {
+ commit(Level.INFO, event);
+ }
+
+ public static void sendError(Map<String, Object> event) throws AuditLoggingException {
+ commit(Level.ERROR, event);
+ }
+
+ public static void sendError(List<Object> keyValues) throws AuditLoggingException {
+ commit(Level.ERROR, toMap(keyValues.toArray()));
+ }
+
+ public static void sendTrace(Map<String, Object> event) throws AuditLoggingException {
+ commit(Level.TRACE, event);
+ }
+
+ public static void sendTrace(List<Object> keyValues) throws AuditLoggingException {
+ commit(Level.TRACE, toMap(keyValues.toArray()));
+ }
+
+ private static void commit(Level level, Map<String, Object> message) throws AuditLoggingException {
+ try {
+ AuditLoggingAPIClient client = instance().apiClient;
+ if (client == null) {
+ // No can do - at least log to file
+ LOGGER.warn("Audit logging server not configured - printing event to log");
+ LOGGER.info(message.toString());
+ } else {
+ switch (level) {
+ case INFO:
+ client.log(message);
+ break;
+ case ERROR:
+ client.error(message);
+ break;
+ case TRACE:
+ client.trace(message);
+ break;
+ default:
+ break;
+ }
+ }
+ } catch (AuditLoggingException e) {
+ // Just for debugging purposes
+ LOGGER.error("Could not send audit event {} with level {}", message, level, e);
+ // log this locally to a file just in case
+ AuditLogging.log("local", message);
+ throw e;
+ }
+ }
+}
--- /dev/null
+package org.simantics.audit.server;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Response;
+
+import org.simantics.audit.AuditLogging;
+import org.simantics.audit.AuditLoggingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Path("audit")
+public class AuditLoggingAPI {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AuditLoggingAPI.class);
+
+ private static Map<String, Object> buildJSONResponse(Object... keyValues) {
+ if ((keyValues.length % 2) != 0)
+ throw new IllegalArgumentException("Invalid amount of arguments! " + Arrays.toString(keyValues));
+ Map<String, Object> results = new HashMap<>(keyValues.length / 2);
+ for (int i = 0; i < keyValues.length; i += 2) {
+ Object key = keyValues[i];
+ Object value = keyValues[i + 1];
+ if (!(key instanceof String))
+ throw new IllegalArgumentException("Key with index " + i + " is not String");
+ results.put((String) key, value);
+ }
+ return results;
+ }
+
+ @Path("register")
+ @POST
+ public Response register(Map<String, String> payload) {
+ String id = payload.get("id");
+
+ try {
+ String uuid = AuditLogging.register(id);
+ return Response.ok(buildJSONResponse("uuid", uuid)).build();
+ } catch (AuditLoggingException e) {
+ LOGGER.error("Could not register audit with id {}", id, e);
+ return Response.serverError().entity(buildJSONResponse("message", e.getMessage())).build();
+ }
+ }
+
+ @Path("{uuid}/log")
+ @POST
+ public Response log(@PathParam("uuid") String uuid, Map<String, Object> payload) {
+
+ try {
+ AuditLogging.log(uuid, payload);
+ return Response.ok().build();
+ } catch (AuditLoggingException e) {
+ LOGGER.error("Could not log audit with id {}", uuid, e);
+ return Response.serverError().entity(buildJSONResponse("message", e.getMessage())).build();
+ }
+ }
+}
--- /dev/null
+package org.simantics.audit.server;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AuditLoggingServer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AuditLoggingServer.class);
+
+ private static AuditLoggingServer INSTANCE = null;
+ private static Server server;
+ private static ServiceServerThread serverThread;
+
+ private AuditLoggingServer(String token, int preferablePort) {
+ ResourceConfig config = new ResourceConfig();
+ // JSON serialization/deserialization
+ config.register(JacksonFeature.class);
+ // Actual API
+ config.register(AuditLoggingAPI.class);
+ // Authorization
+// config.register(new AuthorizationFilter(token));
+
+ ServletHolder holder = new ServletHolder(new ServletContainer(config));
+
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(preferablePort);
+
+ server.setConnectors(new Connector[] { connector });
+
+ ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
+ context.addServlet(holder, "/*");
+ }
+
+ private static class ServiceServerThread extends Thread {
+
+ @Override
+ public void run() {
+ try {
+ server.start();
+ server.join();
+ } catch (Exception e) {
+ LOGGER.error("Could not start server ", e);
+ }
+ }
+ }
+
+ private static synchronized AuditLoggingServer getInstance(String token, int port) {
+ try {
+ if (INSTANCE == null) {
+ INSTANCE = new AuditLoggingServer(token, port);
+ }
+ } catch (Exception e) {
+ LOGGER.error("Could not initialize SCL REST server", e);
+ }
+ return INSTANCE;
+ }
+
+ public static synchronized void start(String token, int port) throws Exception {
+ // Ensure that an instance is created
+ getInstance(token, port);
+ if (serverThread == null && server != null) {
+ serverThread = new ServiceServerThread();
+ serverThread.start();
+ }
+ }
+
+ public static synchronized void stop() throws Exception {
+ if (server != null)
+ server.stop();
+ serverThread = null;
+ }
+}
<module>org.simantics.annotation.ontology</module>
<module>org.simantics.annotation.ui</module>
<module>org.simantics.application</module>
+ <module>org.simantics.auditlogging</module>
<module>org.simantics.backup</module>
<module>org.simantics.backup.db</module>
<module>org.simantics.backup.ontology</module>
version="0.0.0"
unpack="false"/>
+ <plugin
+ id="org.simantics.auditlogging"
+ download-size="0"
+ install-size="0"
+ version="0.0.0"
+ unpack="false"/>
+
</feature>