From 754a9715adb314462edd07d24d34a3c1e13e9ea2 Mon Sep 17 00:00:00 2001 From: jonathanl-bq <72158117+jonathanl-bq@users.noreply.github.com> Date: Tue, 2 Jul 2024 10:26:43 -0700 Subject: [PATCH] Java: Add Logger (#1422) * Implement Logger for Java client Signed-off-by: Yury-Fridlyand Co-authored-by: Yury-Fridlyand Co-authored-by: Andrew Carbonetto --- .../main/java/glide/api/logging/Logger.java | 219 ++++++++++++++++++ .../handlers/CallbackDispatcher.java | 15 +- .../connectors/handlers/MessageHandler.java | 43 ++-- .../connectors/handlers/ReadHandler.java | 7 +- .../glide/ffi/resolvers/LoggerResolver.java | 13 ++ java/client/src/main/java/module-info.java | 1 + .../java/glide/ExceptionHandlingTests.java | 12 + .../ConnectionWithGlideMockTests.java | 3 + java/integTest/build.gradle | 1 - .../src/test/java/glide/LoggerTests.java | 86 +++++++ java/src/errors.rs | 2 + java/src/lib.rs | 104 ++++++++- 12 files changed, 477 insertions(+), 29 deletions(-) create mode 100644 java/client/src/main/java/glide/api/logging/Logger.java create mode 100644 java/client/src/main/java/glide/ffi/resolvers/LoggerResolver.java create mode 100644 java/integTest/src/test/java/glide/LoggerTests.java diff --git a/java/client/src/main/java/glide/api/logging/Logger.java b/java/client/src/main/java/glide/api/logging/Logger.java new file mode 100644 index 0000000000..744c5c2ddc --- /dev/null +++ b/java/client/src/main/java/glide/api/logging/Logger.java @@ -0,0 +1,219 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.logging; + +import static glide.ffi.resolvers.LoggerResolver.initInternal; +import static glide.ffi.resolvers.LoggerResolver.logInternal; + +import java.util.function.Supplier; +import lombok.Getter; +import lombok.NonNull; + +/** + * A singleton class that allows logging which is consistent with logs from the internal rust core. + * The logger can be set up in 2 ways - + * + *
    + *
  1. By calling Logger.init, which configures the logger only if it wasn't + * previously configured. + *
  2. By calling Logger.setLoggerConfig, which replaces the existing configuration, + * and means that new logs will not be saved with the logs that were sent before the call. + *
+ * + * If setLoggerConfig wasn't called, the first log attempt will initialize a new logger + * with default configuration decided by Glide core. + */ +public final class Logger { + @Getter + public enum Level { + DISABLED(-2), + DEFAULT(-1), + ERROR(0), + WARN(1), + INFO(2), + DEBUG(3), + TRACE(4); + + private final int level; + + Level(int level) { + this.level = level; + } + + public static Level fromInt(int i) { + switch (i) { + case 0: + return ERROR; + case 1: + return WARN; + case 2: + return INFO; + case 3: + return DEBUG; + case 4: + return TRACE; + default: + return DEFAULT; + } + } + } + + @Getter private static Level loggerLevel; + + private static void initLogger(@NonNull Level level, String fileName) { + if (level == Level.DISABLED) { + loggerLevel = level; + return; + } + loggerLevel = Level.fromInt(initInternal(level.getLevel(), fileName)); + } + + /** + * Initialize a logger if it wasn't initialized before - this method is meant to be used when + * there is no intention to replace an existing logger. The logger will filter all logs with a + * level lower than the given level. If given a fileName argument, will write the + * logs to files postfixed with fileName. If fileName isn't provided, + * the logs will be written to the console. + * + * @param level Set the logger level to one of + * [DISABLED, DEFAULT, ERROR, WARN, INFO, DEBUG, TRACE] + * . If log level isn't provided, the logger will be configured with default + * configuration decided by Glide core. + * @param fileName If provided, the target of the logs will be the file mentioned. Otherwise, logs + * will be printed to the console. + */ + public static void init(@NonNull Level level, String fileName) { + if (loggerLevel == null) { + initLogger(level, fileName); + } + } + + /** + * Initialize a logger if it wasn't initialized before - this method is meant to be used when + * there is no intention to replace an existing logger. The logger will filter all logs with a + * level lower than the default level decided by Glide core. Given a fileName + * argument, will write the logs to files postfixed with fileName. + * + * @param fileName The target of the logs will be the file mentioned. + */ + public static void init(@NonNull String fileName) { + init(Level.DEFAULT, fileName); + } + + /** + * Initialize a logger if it wasn't initialized before - this method is meant to be used when + * there is no intention to replace an existing logger. The logger will filter all logs with a + * level lower than the default level decided by Glide core. The logs will be written to stdout. + */ + public static void init() { + init(Level.DEFAULT, null); + } + + /** + * Initialize a logger if it wasn't initialized before - this method is meant to be used when + * there is no intention to replace an existing logger. The logger will filter all logs with a + * level lower than the given level. The logs will be written to stdout. + * + * @param level Set the logger level to one of [DEFAULT, ERROR, WARN, INFO, DEBUG, TRACE] + * . If log level isn't provided, the logger will be configured with default + * configuration decided by Glide core. + */ + public static void init(@NonNull Level level) { + init(level, null); + } + + /** + * Logs the provided message if the provided log level is lower than the logger level. This + * overload takes a Supplier to lazily construct the message. + * + * @param level The log level of the provided message. + * @param logIdentifier The log identifier should give the log a context. + * @param messageSupplier The Supplier of the message to log. + */ + public static void log( + @NonNull Level level, + @NonNull String logIdentifier, + @NonNull Supplier messageSupplier) { + if (loggerLevel == null) { + initLogger(Level.DEFAULT, null); + } + + if (level == Level.DISABLED) { + return; + } + + if (!(level.getLevel() <= loggerLevel.getLevel())) { + return; + } + logInternal(level.getLevel(), logIdentifier, messageSupplier.get()); + } + + /** + * Logs the provided message if the provided log level is lower than the logger level. + * + * @param level The log level of the provided message. + * @param logIdentifier The log identifier should give the log a context. + * @param message The message to log. + */ + public static void log( + @NonNull Level level, @NonNull String logIdentifier, @NonNull String message) { + if (loggerLevel == null) { + initLogger(Level.DEFAULT, null); + } + + if (level == Level.DISABLED) { + return; + } + + if (!(level.getLevel() <= loggerLevel.getLevel())) { + return; + } + logInternal(level.getLevel(), logIdentifier, message); + } + + /** + * Creates a new logger instance and configure it with the provided log level and file name. + * + * @param level Set the logger level to one of + * [DISABLED, DEFAULT, ERROR, WARN, INFO, DEBUG, TRACE] + * . If log level isn't provided, the logger will be configured with default + * configuration decided by Glide core. + * @param fileName If provided, the target of the logs will be the file mentioned. Otherwise, logs + * will be printed to stdout. + */ + public static void setLoggerConfig(@NonNull Level level, String fileName) { + initLogger(level, fileName); + } + + /** + * Creates a new logger instance and configure it with the provided log level. The logs will be + * written to stdout. + * + * @param level Set the logger level to one of + * [DISABLED, DEFAULT, ERROR, WARN, INFO, DEBUG, TRACE] + * . If log level isn't provided, the logger will be configured with default + * configuration decided by Glide core. + */ + public static void setLoggerConfig(@NonNull Level level) { + setLoggerConfig(level, null); + } + + /** + * Creates a new logger instance and configure it with the provided file name and default log + * level. The logger will filter all logs with a level lower than the default level decided by the + * Glide core. + * + * @param fileName If provided, the target of the logs will be the file mentioned. Otherwise, logs + * will be printed to stdout. + */ + public static void setLoggerConfig(String fileName) { + setLoggerConfig(Level.DEFAULT, fileName); + } + + /** + * Creates a new logger instance. The logger will filter all logs with a level lower than the + * default level decided by Glide core. The logs will be written to stdout. + */ + public static void setLoggerConfig() { + setLoggerConfig(Level.DEFAULT, null); + } +} diff --git a/java/client/src/main/java/glide/connectors/handlers/CallbackDispatcher.java b/java/client/src/main/java/glide/connectors/handlers/CallbackDispatcher.java index 03b499dd66..cfd349afdf 100644 --- a/java/client/src/main/java/glide/connectors/handlers/CallbackDispatcher.java +++ b/java/client/src/main/java/glide/connectors/handlers/CallbackDispatcher.java @@ -1,6 +1,9 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.handlers; +import static glide.api.logging.Logger.Level.ERROR; + +import glide.api.logging.Logger; import glide.api.models.exceptions.ClosingException; import glide.api.models.exceptions.ConnectionException; import glide.api.models.exceptions.ExecAbortException; @@ -115,11 +118,15 @@ public void completeRequest(Response response) { } future.completeAsync(() -> response); } else { - // TODO: log an error thru logger. // probably a response was received after shutdown or `registerRequest` call was missing - System.err.printf( - "Received a response for not registered callback id %d, request error = %s%n", - callbackId, response.getRequestError()); + Logger.log( + ERROR, + "callback dispatcher", + () -> + "Received a response for not registered callback id " + + callbackId + + ", request error = " + + response.getRequestError()); distributeClosingException("Client is in an erroneous state and should close"); } } diff --git a/java/client/src/main/java/glide/connectors/handlers/MessageHandler.java b/java/client/src/main/java/glide/connectors/handlers/MessageHandler.java index 74de6ddeb0..da2d16502a 100644 --- a/java/client/src/main/java/glide/connectors/handlers/MessageHandler.java +++ b/java/client/src/main/java/glide/connectors/handlers/MessageHandler.java @@ -1,6 +1,7 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.handlers; +import glide.api.logging.Logger; import glide.api.models.PubSubMessage; import glide.api.models.configuration.BaseSubscriptionConfiguration.MessageCallback; import glide.api.models.exceptions.RedisException; @@ -39,8 +40,10 @@ public class MessageHandler { public void handle(Response response) { Object data = responseResolver.apply(response); if (!(data instanceof Map)) { - // TODO log thru logger https://github.com/aws/glide-for-redis/pull/1422 - System.err.println("Received invalid push: empty or in incorrect format."); + Logger.log( + Logger.Level.WARN, + "invalid push", + "Received invalid push: empty or in incorrect format."); throw new RedisException("Received invalid push: empty or in incorrect format."); } @SuppressWarnings("unchecked") @@ -50,11 +53,10 @@ public void handle(Response response) { switch (pushType) { case Disconnection: - // TODO log thru logger https://github.com/aws/glide-for-redis/pull/1422 - // ClientLogger.log( - // LogLevel.WARN, - // "disconnect notification", - // "Transport disconnected, messages might be lost", + Logger.log( + Logger.Level.WARN, + "disconnect notification", + "Transport disconnected, messages might be lost"); break; case PMessage: handle(new PubSubMessage((String) values[2], (String) values[1], (String) values[0])); @@ -70,21 +72,22 @@ public void handle(Response response) { case PUnsubscribe: case SUnsubscribe: // ignore for now - // TODO log thru logger https://github.com/aws/glide-for-redis/pull/1422 - System.out.printf( - "Received push notification of type '%s': %s\n", - pushType, - Arrays.stream(values) - .map(v -> v instanceof Number ? v.toString() : String.format("'%s'", v)) - .collect(Collectors.joining(" "))); + Logger.log( + Logger.Level.INFO, + "subscribe/unsubscribe notification", + () -> + String.format( + "Received push notification of type '%s': %s", + pushType, + Arrays.stream(values) + .map(v -> v instanceof Number ? v.toString() : String.format("'%s'", v)) + .collect(Collectors.joining(" ")))); break; default: - // TODO log thru logger https://github.com/aws/glide-for-redis/pull/1422 - System.err.printf("Received push with unsupported type: %s.\n", pushType); - // ClientLogger.log( - // LogLevel.WARN, - // "unknown notification", - // f"Unknown notification message: '{message_kind}'", + Logger.log( + Logger.Level.WARN, + "unknown notification", + () -> String.format("Unknown notification message: '%s'", pushType)); } } diff --git a/java/client/src/main/java/glide/connectors/handlers/ReadHandler.java b/java/client/src/main/java/glide/connectors/handlers/ReadHandler.java index ca801a6094..d78d144cea 100644 --- a/java/client/src/main/java/glide/connectors/handlers/ReadHandler.java +++ b/java/client/src/main/java/glide/connectors/handlers/ReadHandler.java @@ -1,6 +1,9 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.connectors.handlers; +import static glide.api.logging.Logger.Level.ERROR; + +import glide.api.logging.Logger; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import lombok.NonNull; @@ -29,9 +32,7 @@ public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) /** Handles uncaught exceptions from {@link #channelRead(ChannelHandlerContext, Object)}. */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - // TODO: log thru logger - System.out.printf("=== exceptionCaught %s %s %n", ctx, cause); - cause.printStackTrace(); + Logger.log(ERROR, "read handler", () -> "=== exceptionCaught " + ctx + " " + cause); callbackDispatcher.distributeClosingException( "An unhandled error while reading from UDS channel: " + cause); diff --git a/java/client/src/main/java/glide/ffi/resolvers/LoggerResolver.java b/java/client/src/main/java/glide/ffi/resolvers/LoggerResolver.java new file mode 100644 index 0000000000..4c1e2469cc --- /dev/null +++ b/java/client/src/main/java/glide/ffi/resolvers/LoggerResolver.java @@ -0,0 +1,13 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.ffi.resolvers; + +public class LoggerResolver { + // TODO: consider lazy loading the glide_rs library + static { + NativeUtils.loadGlideLib(); + } + + public static native int initInternal(int level, String fileName); + + public static native void logInternal(int level, String logIdentifier, String message); +} diff --git a/java/client/src/main/java/module-info.java b/java/client/src/main/java/module-info.java index 2dbaca04f7..99c4655082 100644 --- a/java/client/src/main/java/module-info.java +++ b/java/client/src/main/java/module-info.java @@ -1,6 +1,7 @@ module glide.api { exports glide.api; exports glide.api.commands; + exports glide.api.logging; exports glide.api.models; exports glide.api.models.commands; exports glide.api.models.commands.bitmap; diff --git a/java/client/src/test/java/glide/ExceptionHandlingTests.java b/java/client/src/test/java/glide/ExceptionHandlingTests.java index f59b50af4d..d383797bd5 100644 --- a/java/client/src/test/java/glide/ExceptionHandlingTests.java +++ b/java/client/src/test/java/glide/ExceptionHandlingTests.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mockStatic; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static response.ResponseOuterClass.RequestErrorType.Disconnect; import static response.ResponseOuterClass.RequestErrorType.ExecAbort; @@ -13,6 +14,7 @@ import static response.ResponseOuterClass.RequestErrorType.Unspecified; import connection_request.ConnectionRequestOuterClass; +import glide.api.logging.Logger; import glide.api.models.configuration.RedisClientConfiguration; import glide.api.models.exceptions.ClosingException; import glide.api.models.exceptions.ConnectionException; @@ -34,19 +36,24 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Stream; import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.MockedStatic; import redis_request.RedisRequestOuterClass.RedisRequest; import response.ResponseOuterClass.RequestError; import response.ResponseOuterClass.RequestErrorType; import response.ResponseOuterClass.Response; public class ExceptionHandlingTests { + private MockedStatic mockedLogger; + @BeforeEach public void init() { + mockedLogger = mockStatic(Logger.class); var threadPoolResource = ThreadPoolResourceAllocator.getOrCreate(() -> null); if (threadPoolResource != null) { threadPoolResource.getEventLoopGroup().shutdownGracefully(); @@ -54,6 +61,11 @@ public void init() { } } + @AfterEach + public void teardown() { + mockedLogger.close(); + } + /** * This test shows how exception handling works in the middle of future pipeline The client has * similar stuff, but it rethrows an exception. diff --git a/java/client/src/test/java/glide/connection/ConnectionWithGlideMockTests.java b/java/client/src/test/java/glide/connection/ConnectionWithGlideMockTests.java index 86af8e7c05..ebee688e69 100644 --- a/java/client/src/test/java/glide/connection/ConnectionWithGlideMockTests.java +++ b/java/client/src/test/java/glide/connection/ConnectionWithGlideMockTests.java @@ -11,6 +11,7 @@ import connection_request.ConnectionRequestOuterClass.ConnectionRequest; import connection_request.ConnectionRequestOuterClass.NodeAddress; import glide.api.RedisClient; +import glide.api.logging.Logger; import glide.api.models.exceptions.ClosingException; import glide.connectors.handlers.CallbackDispatcher; import glide.connectors.handlers.ChannelHandler; @@ -37,6 +38,8 @@ public class ConnectionWithGlideMockTests extends RustCoreLibMockTestBase { @BeforeEach @SneakyThrows public void createTestClient() { + // TODO: Add DISABLED level to logger-core + Logger.setLoggerConfig(Logger.Level.DISABLED); channelHandler = new ChannelHandler( new CallbackDispatcher(null), diff --git a/java/integTest/build.gradle b/java/integTest/build.gradle index ad1a51ada9..32e9258ce1 100644 --- a/java/integTest/build.gradle +++ b/java/integTest/build.gradle @@ -136,7 +136,6 @@ tasks.withType(Test) { events "started", "skipped", "passed", "failed" showStandardStreams true } - jvmArgs "-Djava.library.path=${project.rootDir}/target/release" afterTest { desc, result -> logger.quiet "${desc.className}.${desc.name}: ${result.resultType} ${(result.getEndTime() - result.getStartTime())/1000}s" } diff --git a/java/integTest/src/test/java/glide/LoggerTests.java b/java/integTest/src/test/java/glide/LoggerTests.java new file mode 100644 index 0000000000..02c4a35f6d --- /dev/null +++ b/java/integTest/src/test/java/glide/LoggerTests.java @@ -0,0 +1,86 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import glide.api.logging.Logger; +import java.io.File; +import java.util.Scanner; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +public class LoggerTests { + + private final Logger.Level DEFAULT_TEST_LOG_LEVEL = Logger.Level.WARN; + + @Test + public void init_logger() { + Logger.init(DEFAULT_TEST_LOG_LEVEL); + assertEquals(DEFAULT_TEST_LOG_LEVEL, Logger.getLoggerLevel()); + // The logger is already configured, so calling init again shouldn't modify the log level + Logger.init(Logger.Level.ERROR); + assertEquals(DEFAULT_TEST_LOG_LEVEL, Logger.getLoggerLevel()); + } + + @Test + public void set_logger_config() { + Logger.setLoggerConfig(Logger.Level.INFO); + assertEquals(Logger.Level.INFO, Logger.getLoggerLevel()); + // Revert to the default test log level + Logger.setLoggerConfig(DEFAULT_TEST_LOG_LEVEL); + assertEquals(DEFAULT_TEST_LOG_LEVEL, Logger.getLoggerLevel()); + } + + @SneakyThrows + @Test + public void log_to_file() { + String infoIdentifier = "Info"; + String infoMessage = "foo"; + String warnIdentifier = "Warn"; + String warnMessage = "woof"; + String errorIdentifier = "Error"; + String errorMessage = "meow"; + String debugIdentifier = "Debug"; + String debugMessage = "chirp"; + String traceIdentifier = "Trace"; + String traceMessage = "squawk"; + + Logger.setLoggerConfig(Logger.Level.INFO, "log.txt"); + Logger.log(Logger.Level.INFO, infoIdentifier, infoMessage); + Logger.log(Logger.Level.WARN, warnIdentifier, warnMessage); + Logger.log(Logger.Level.ERROR, errorIdentifier, errorMessage); + Logger.log(Logger.Level.DEBUG, debugIdentifier, debugMessage); + Logger.log(Logger.Level.TRACE, traceIdentifier, traceMessage); + + // Test logging with lazily constructed messages + Logger.log(Logger.Level.INFO, infoIdentifier, () -> infoMessage); + Logger.log(Logger.Level.WARN, warnIdentifier, () -> warnMessage); + Logger.log(Logger.Level.ERROR, errorIdentifier, () -> errorMessage); + Logger.log(Logger.Level.DEBUG, debugIdentifier, () -> debugMessage); + Logger.log(Logger.Level.TRACE, traceIdentifier, () -> traceMessage); + + File logFolder = new File("glide-logs"); + File[] logFiles = logFolder.listFiles((dir, name) -> name.startsWith("log.txt.")); + assertNotNull(logFiles); + File logFile = logFiles[0]; + logFile.deleteOnExit(); + Scanner reader = new Scanner(logFile); + String infoLine = reader.nextLine(); + assertTrue(infoLine.contains(infoIdentifier + " - " + infoMessage)); + String warnLine = reader.nextLine(); + assertTrue(warnLine.contains(warnIdentifier + " - " + warnMessage)); + String errorLine = reader.nextLine(); + assertTrue(errorLine.contains(errorIdentifier + " - " + errorMessage)); + String infoLineLazy = reader.nextLine(); + assertTrue(infoLineLazy.contains(infoIdentifier + " - " + infoMessage)); + String warnLineLazy = reader.nextLine(); + assertTrue(warnLineLazy.contains(warnIdentifier + " - " + warnMessage)); + String errorLineLazy = reader.nextLine(); + assertTrue(errorLineLazy.contains(errorIdentifier + " - " + errorMessage)); + assertFalse(reader.hasNextLine()); + reader.close(); + } +} diff --git a/java/src/errors.rs b/java/src/errors.rs index d4d58ac554..93372a7257 100644 --- a/java/src/errors.rs +++ b/java/src/errors.rs @@ -9,6 +9,7 @@ pub enum FFIError { Jni(JNIError), Uds(String), Utf8(FromUtf8Error), + Logger(String), } impl From for FFIError { @@ -29,6 +30,7 @@ impl std::fmt::Display for FFIError { FFIError::Jni(err) => write!(f, "{}", err), FFIError::Uds(err) => write!(f, "{}", err), FFIError::Utf8(err) => write!(f, "{}", err), + FFIError::Logger(err) => write!(f, "{}", err), } } } diff --git a/java/src/lib.rs b/java/src/lib.rs index 2fc6e441be..f8e6cda231 100644 --- a/java/src/lib.rs +++ b/java/src/lib.rs @@ -5,8 +5,9 @@ use glide_core::start_socket_listener as start_socket_listener_core; use glide_core::MAX_REQUEST_ARGS_LENGTH as MAX_REQUEST_ARGS_LENGTH_IN_BYTES; use bytes::Bytes; +use jni::errors::Error as JniError; use jni::objects::{JByteArray, JClass, JObject, JObjectArray, JString}; -use jni::sys::{jlong, jsize}; +use jni::sys::{jint, jlong, jsize}; use jni::JNIEnv; use redis::Value; use std::sync::mpsc; @@ -20,6 +21,8 @@ mod ffi_test; #[cfg(ffi_test)] pub use ffi_test::*; +struct Level(i32); + // TODO: Consider caching method IDs here in a static variable (might need RwLock to mutate) fn redis_value_to_java<'local>( env: &mut JNIEnv<'local>, @@ -316,3 +319,102 @@ pub extern "system" fn Java_glide_ffi_resolvers_ScriptResolver_dropScript<'local ) .unwrap_or(()) } + +// TODO: Add DISABLED level here once it is added to logger-core +impl From for Level { + fn from(level: logger_core::Level) -> Self { + match level { + logger_core::Level::Error => Level(0), + logger_core::Level::Warn => Level(1), + logger_core::Level::Info => Level(2), + logger_core::Level::Debug => Level(3), + logger_core::Level::Trace => Level(4), + } + } +} + +impl TryFrom for logger_core::Level { + type Error = FFIError; + fn try_from(level: Level) -> Result>::Error> { + // TODO: Add DISABLED level here once it is added to logger-core + match level.0 { + 0 => Ok(logger_core::Level::Error), + 1 => Ok(logger_core::Level::Warn), + 2 => Ok(logger_core::Level::Info), + 3 => Ok(logger_core::Level::Debug), + 4 => Ok(logger_core::Level::Trace), + _ => Err(FFIError::Logger(format!( + "Invalid log level: {:?}", + level.0 + ))), + } + } +} + +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_LoggerResolver_logInternal<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + level: jint, + log_identifier: JString<'local>, + message: JString<'local>, +) { + handle_panics( + move || { + fn log_internal( + env: &mut JNIEnv<'_>, + level: jint, + log_identifier: JString<'_>, + message: JString<'_>, + ) -> Result<(), FFIError> { + let level = Level(level); + + let log_identifier: String = env.get_string(&log_identifier)?.into(); + + let message: String = env.get_string(&message)?.into(); + + logger_core::log(level.try_into()?, log_identifier, message); + Ok(()) + } + let result = log_internal(&mut env, level, log_identifier, message); + handle_errors(&mut env, result) + }, + "logInternal", + ) + .unwrap_or(()) +} + +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_LoggerResolver_initInternal<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + level: jint, + file_name: JString<'local>, +) -> jint { + handle_panics( + move || { + fn init_internal( + env: &mut JNIEnv<'_>, + level: jint, + file_name: JString<'_>, + ) -> Result { + let level = if level >= 0 { Some(level) } else { None }; + let file_name: Option = match env.get_string(&file_name) { + Ok(file_name) => Some(file_name.into()), + Err(JniError::NullPtr(_)) => None, + Err(err) => return Err(err.into()), + }; + let level = match level { + Some(lvl) => Some(Level(lvl).try_into()?), + None => None, + }; + let logger_level = logger_core::init(level, file_name.as_deref()); + Ok(Level::from(logger_level).0) + } + let result = init_internal(&mut env, level, file_name); + handle_errors(&mut env, result) + }, + "initInternal", + ) + .unwrap_or(0) +}