diff --git a/.gitignore b/.gitignore index f83d40cc..98e981ec 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *~ .DS_Store /bazel-* +.ijwb diff --git a/README.md b/README.md index 3534be5e..1c6cdba6 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ be `runtime` scope. ### 2. Add an import for [`FluentLogger`] ```java -import com.google.common.flogger.FluentLogger; + ``` ### 3. Create a `private static final` instance diff --git a/api/BUILD b/api/BUILD index 3d4b9d0f..aa0c7e42 100644 --- a/api/BUILD +++ b/api/BUILD @@ -137,6 +137,17 @@ java_binary( deps = ["@google_bazel_common//third_party/java/asm"], ) +java_binary( + name = "fluent_aggregated_logger_example", + srcs = ["src/main/java/com/google/common/flogger/example/FluentAggregatedLoggerExample.java"], + main_class = "com.google.common.flogger.example.FluentAggregatedLoggerExample", + deps = [ + ":api", + ":system_backend", + "@google_bazel_common//third_party/java/log4j", + ], +) + genrule( name = "gen_platform_provider", outs = ["platform_provider.jar"], diff --git a/api/src/main/java/com/google/common/flogger/AbstractLogger.java b/api/src/main/java/com/google/common/flogger/AbstractLogger.java index bfc0fdce..a5aab927 100644 --- a/api/src/main/java/com/google/common/flogger/AbstractLogger.java +++ b/api/src/main/java/com/google/common/flogger/AbstractLogger.java @@ -16,22 +16,21 @@ package com.google.common.flogger; -import static com.google.common.flogger.util.Checks.checkNotNull; - import com.google.common.flogger.backend.LogData; import com.google.common.flogger.backend.LoggerBackend; import com.google.common.flogger.backend.LoggingException; import com.google.errorprone.annotations.CheckReturnValue; + import java.util.logging.Level; +import static com.google.common.flogger.util.Checks.checkNotNull; + /** * Base class for the fluent logger API. This class is a factory for instances of a fluent logging * API, used to build log statements via method chaining. - * - * @param the logging API provided by this logger. */ @CheckReturnValue -public abstract class AbstractLogger> { +public abstract class AbstractLogger { private final LoggerBackend backend; /** @@ -43,61 +42,6 @@ protected AbstractLogger(LoggerBackend backend) { this.backend = checkNotNull(backend, "backend"); } - // ---- PUBLIC API ---- - - /** - * Returns a fluent logging API appropriate for the specified log level. - *

- * If a logger implementation determines that logging is definitely disabled at this point then - * this method is expected to return a "no-op" implementation of that logging API, which will - * result in all further calls made for the log statement to being silently ignored. - *

- * A simple implementation of this method in a concrete subclass might look like: - *

{@code
-   * boolean isLoggable = isLoggable(level);
-   * boolean isForced = Platform.shouldForceLogging(getName(), level, isLoggable);
-   * return (isLoggable | isForced) ? new SubContext(level, isForced) : NO_OP;
-   * }
- * where {@code NO_OP} is a singleton, no-op instance of the logging API whose methods do nothing - * and just {@code return noOp()}. - */ - public abstract API at(Level level); - - /** A convenience method for at({@link Level#SEVERE}). */ - public final API atSevere() { - return at(Level.SEVERE); - } - - /** A convenience method for at({@link Level#WARNING}). */ - public final API atWarning() { - return at(Level.WARNING); - } - - /** A convenience method for at({@link Level#INFO}). */ - public final API atInfo() { - return at(Level.INFO); - } - - /** A convenience method for at({@link Level#CONFIG}). */ - public final API atConfig() { - return at(Level.CONFIG); - } - - /** A convenience method for at({@link Level#FINE}). */ - public final API atFine() { - return at(Level.FINE); - } - - /** A convenience method for at({@link Level#FINER}). */ - public final API atFiner() { - return at(Level.FINER); - } - - /** A convenience method for at({@link Level#FINEST}). */ - public final API atFinest() { - return at(Level.FINEST); - } - // ---- HELPER METHODS (useful during sub-class initialization) ---- /** @@ -115,14 +59,6 @@ protected String getName() { return backend.getLoggerName(); } - /** - * Returns whether the given level is enabled for this logger. Users wishing to guard code with a - * check for "loggability" should use {@code logger.atLevel().isEnabled()} instead. - */ - protected final boolean isLoggable(Level level) { - return backend.isLoggable(level); - } - // ---- IMPLEMENTATION DETAIL (only visible to the base logging context) ---- /** diff --git a/api/src/main/java/com/google/common/flogger/AbstractMessageLogger.java b/api/src/main/java/com/google/common/flogger/AbstractMessageLogger.java new file mode 100644 index 00000000..51ef2113 --- /dev/null +++ b/api/src/main/java/com/google/common/flogger/AbstractMessageLogger.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2012 The Flogger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.common.flogger; + +import static com.google.common.flogger.util.Checks.checkNotNull; + +import com.google.common.flogger.backend.LogData; +import com.google.common.flogger.backend.LoggerBackend; +import com.google.common.flogger.backend.LoggingException; +import com.google.errorprone.annotations.CheckReturnValue; +import java.util.logging.Level; + +/** + * Base class for the fluent logger API. This class is a factory for instances of a fluent logging + * API, used to build log statements via method chaining. + * + * @param the logging API provided by this logger. + */ +@CheckReturnValue +public abstract class AbstractMessageLogger> extends AbstractLogger{ + + /** + * Constructs a new logger for the specified backend. + * + * @param backend the logger backend which ultimately writes the log statements out. + */ + protected AbstractMessageLogger(LoggerBackend backend) { + super(backend); + } + + // ---- PUBLIC API ---- + + /** + * Returns a fluent logging API appropriate for the specified log level. + *

+ * If a logger implementation determines that logging is definitely disabled at this point then + * this method is expected to return a "no-op" implementation of that logging API, which will + * result in all further calls made for the log statement to being silently ignored. + *

+ * A simple implementation of this method in a concrete subclass might look like: + *

{@code
+   * boolean isLoggable = isLoggable(level);
+   * boolean isForced = Platform.shouldForceLogging(getName(), level, isLoggable);
+   * return (isLoggable | isForced) ? new SubContext(level, isForced) : NO_OP;
+   * }
+ * where {@code NO_OP} is a singleton, no-op instance of the logging API whose methods do nothing + * and just {@code return noOp()}. + */ + public abstract API at(Level level); + + /** A convenience method for at({@link Level#SEVERE}). */ + public final API atSevere() { + return at(Level.SEVERE); + } + + /** A convenience method for at({@link Level#WARNING}). */ + public final API atWarning() { + return at(Level.WARNING); + } + + /** A convenience method for at({@link Level#INFO}). */ + public final API atInfo() { + return at(Level.INFO); + } + + /** A convenience method for at({@link Level#CONFIG}). */ + public final API atConfig() { + return at(Level.CONFIG); + } + + /** A convenience method for at({@link Level#FINE}). */ + public final API atFine() { + return at(Level.FINE); + } + + /** A convenience method for at({@link Level#FINER}). */ + public final API atFiner() { + return at(Level.FINER); + } + + /** A convenience method for at({@link Level#FINEST}). */ + public final API atFinest() { + return at(Level.FINEST); + } + + protected final boolean isLoggable(Level level) { + return getBackend().isLoggable(level); + } +} diff --git a/api/src/main/java/com/google/common/flogger/AggregatedLogContext.java b/api/src/main/java/com/google/common/flogger/AggregatedLogContext.java new file mode 100644 index 00000000..4a21b381 --- /dev/null +++ b/api/src/main/java/com/google/common/flogger/AggregatedLogContext.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2020 The Flogger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.common.flogger; + +import com.google.common.flogger.backend.LogData; +import com.google.common.flogger.backend.Metadata; +import com.google.common.flogger.backend.Platform; +import com.google.common.flogger.backend.TemplateContext; +import com.google.common.flogger.parser.DefaultPrintfMessageParser; +import com.google.common.flogger.util.Checks; +import com.google.errorprone.annotations.CheckReturnValue; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.google.common.flogger.util.Checks.checkNotNull; + +/** + * Base class for the aggregated logger API. + * + * @param the {@link AbstractLogger} to write log data + * @param the {@link AggregatedLoggingApi} to format log data + */ +@CheckReturnValue +public abstract class AggregatedLogContext implements AggregatedLoggingApi { + + protected final MutableMetadata metadata = new MutableMetadata(); + protected final String name; + protected final FluentAggregatedLogger logger; + protected final LogSite logSite; + protected final ScheduledExecutorService pool; + //Runnable for time window + private volatile LogFlusher flusher; + //Only one thread will flush log + private AtomicBoolean flushLock = new AtomicBoolean(false); + /** + * Instantiates a new AggregatedLogContext. + * + * @param name the name + * @param logger the logger (see {@link FluentAggregatedLogger}). + * @param logSite the log site (see {@link LogSite}). + * @param pool the executor service pool used to periodically log data. + */ + protected AggregatedLogContext(String name, FluentAggregatedLogger logger, LogSite logSite, ScheduledExecutorService pool) { + this.name = checkNotNull(name, "name"); + this.logger = checkNotNull(logger, "logger"); + this.logSite = checkNotNull(logSite, "logSite"); + this.pool = checkNotNull(pool, "pool"); + } + + protected abstract API self(); + + public String getName() { + return name; + } + + /** + * Schedule log flusher at fixed rate of time window. + * Please call withTimeWindow() before start() to set time window. + */ + public synchronized API start() { + if (flusher != null) { + return self(); + } + + int period = getTimeWindow(); + flusher = new LogFlusher(); + pool.scheduleAtFixedRate(flusher, 2, period, TimeUnit.SECONDS); + + return self(); + } + + @Override + public API withTimeWindow(int seconds) { + if (flusher != null) { + throw new RuntimeException("Please do not change time window after logger start."); + } + + Checks.checkArgument(seconds > 0 && seconds <= 3600, + "Time window range should be (0,3600]"); + + metadata.addValue(Key.TIME_WINDOW, seconds); + + return self(); + } + + @Override + public API withNumberWindow(int number) { + Checks.checkArgument(number > 0 && number <= 1000 * 1000, + "Number window range should be (0, 1000000])"); + + metadata.addValue(Key.NUMBER_WINDOW, number); + return self(); + } + + /** + * Get time window configuration. Default value is 60(seconds). + */ + @Override + public int getTimeWindow() { + Integer timeWindow = metadata.findValue(Key.TIME_WINDOW); + return timeWindow == null ? 60 : timeWindow; // Default 60 seconds + } + + /** + * Get number window configuration. Default value is 100. + */ + @Override + public int getNumberWindow() { + Integer numberWindow = metadata.findValue(Key.NUMBER_WINDOW); + return numberWindow == null ? 100 : numberWindow; // Default 100 + } + + /** + * Gets metadata. + * + * @return the metadata + */ + protected Metadata getMetadata() { + return metadata != null ? metadata : Metadata.empty(); + + } + + /** + * Gets logger. + * + * @return the logger + */ + protected FluentAggregatedLogger getLogger() { + return logger; + } + + /** + * Generate {@link LogData} for logger backend. + * + * @return the log data + */ + protected LogData getLogData(String message) { + long timestampNanos = Platform.getCurrentTimeNanos(); + String loggerName = getLogger().getBackend().getLoggerName(); + + DefaultLogData logData = new DefaultLogData(timestampNanos, loggerName); + logData.setMetadata(getMetadata()); + + logData.setLogSite(logSite); + logData.setTemplateContext(new TemplateContext(DefaultPrintfMessageParser.getInstance(), message)); + + //use empty array for avoiding null exception + logData.setArgs(new Object[]{}); + + return logData; + } + + /** + * Flush aggregated data in new Thread. + */ + protected void asyncFlush(final int count) { + new Thread(new Runnable() { + @Override + public void run() { + flush(count); + } + }).start(); + } + + /** + * Visible for test.Because real logging action is done in different thread, + * junit will not get the async logging data when running unit test. + * So call log method directly in junit testcase. + */ + protected void flush(int count) { + if (flushLock.compareAndSet(false, true)) { + try { + if (count > 0 && haveData() < count) { + return; + } + + LogData logData = getLogData(message(count)); + getLogger().write(logData); + } finally { + flushLock.compareAndSet(true, false); + } + } + } + + /** + * Available configuration Key + */ + public static final class Key { + /** + * Time window for aggregating log. + * {@link AbstractLogger} will log all data at the end of the time window. + */ + public static final MetadataKey TIME_WINDOW = + MetadataKey.single("time_window", Integer.class); + /** + * Number window for aggregating log. + * {@link AbstractLogger} will log data when the number of data is up to number window. + */ + public static final MetadataKey NUMBER_WINDOW = + MetadataKey.single("number_window", Integer.class); + + private Key() { + } + } + + private final class LogFlusher implements Runnable { + @Override + public void run() { + flush(0); // Always flush all data + + // Write one more log to show timer is running when there is no any data. + LogData logData = getLogData(getName() + " periodically flush log finished"); + getLogger().write(logData); + } + } +} diff --git a/api/src/main/java/com/google/common/flogger/AggregatedLoggingApi.java b/api/src/main/java/com/google/common/flogger/AggregatedLoggingApi.java new file mode 100644 index 00000000..868ff684 --- /dev/null +++ b/api/src/main/java/com/google/common/flogger/AggregatedLoggingApi.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2020 The Flogger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.common.flogger; + +import com.google.errorprone.annotations.CheckReturnValue; + +/** + * The basic aggregated logging API. An implementation of this API (or an extension of it) will be + * returned by any {@link FluentAggregatedLogger}, and forms the basis of the call chain. + */ +@CheckReturnValue +public interface AggregatedLoggingApi { + /** + * Set aggregated logger time window. + * If time window is set, aggregated logger will periodically flush log. + * + * @param seconds + * @return + */ + API withTimeWindow(int seconds); + + int getTimeWindow(); + + /** + * Set aggregated logger number window. + *

+ * If number window is set, aggregated logger will flush log when log number + * is equal or more than number window. + * + * @param number + * @return + */ + API withNumberWindow(int number); + + int getNumberWindow(); + + /** + * Check if there are enough data to log based on number window configuration. + * + * @return true: flush now; false: not flush. + */ + boolean shouldFlushByNumber(); + + /** + * Check if there are some data to log. + * + * @return the amount of data to be logged + */ + int haveData(); + + /** + * Format aggregated data to string for logging. + * + * @param count the amount of data, 0: all, >0: specified amount + * @return formatted string content for LogData + */ + String message(int count); + +} diff --git a/api/src/main/java/com/google/common/flogger/DefaultLogData.java b/api/src/main/java/com/google/common/flogger/DefaultLogData.java new file mode 100644 index 00000000..3d286842 --- /dev/null +++ b/api/src/main/java/com/google/common/flogger/DefaultLogData.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2020 The Flogger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.common.flogger; + +import com.google.common.flogger.backend.LogData; +import com.google.common.flogger.backend.Metadata; +import com.google.common.flogger.backend.TemplateContext; + +import java.util.logging.Level; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +/** + * Default implementation of LogData. + */ +public class DefaultLogData implements LogData { + private final Level level = Level.INFO; + /** The timestamp of the log statement that this context is associated with. */ + private final long timestampNanos; + + private final String loggerName; + + /** Additional metadata for this log statement (added via fluent API methods). */ + private Metadata metadata = null; + /** The log site information for this log statement (set immediately prior to post-processing). */ + private LogSite logSite = null; + /** The template context if formatting is required (set only after post-processing). */ + private TemplateContext templateContext = null; + /** The log arguments (set only after post-processing). */ + private Object[] args = null; + + public DefaultLogData(long timestampNanos, String loggerName){ + this.timestampNanos = timestampNanos; + this.loggerName = loggerName; + } + + public void setMetadata(Metadata metadata) { + this.metadata = metadata; + } + + public void setLogSite(LogSite logSite) { + this.logSite = logSite; + } + + public void setArgs(Object[] args) { + this.args = args; + } + + @Override + public Level getLevel() { + return level; + } + + @Override + public long getTimestampMicros() { + return NANOSECONDS.toMicros(timestampNanos); + } + + @Override + public long getTimestampNanos() { + return timestampNanos; + } + + @Override + public String getLoggerName() { + return loggerName; + } + + @Override + public LogSite getLogSite() { + if (logSite == null) { + throw new IllegalStateException("cannot request log site information prior to postProcess()"); + } + return logSite; + } + + public void setTemplateContext(TemplateContext templateContext) { + this.templateContext = templateContext; + } + + @Override + public Metadata getMetadata() { + return metadata != null ? metadata : Metadata.empty(); + } + + @Override + public boolean wasForced() { + // Check explicit TRUE here because findValue() can return null (which would fail unboxing). + return metadata != null && Boolean.TRUE.equals(metadata.findValue(LogContext.Key.WAS_FORCED)); + + } + + @Override + public TemplateContext getTemplateContext() { + return templateContext; + } + + @Override + public Object[] getArguments() { + if (templateContext == null) { + throw new IllegalStateException("cannot get arguments unless a template context exists"); + } + return args; + } + + @Override + public Object getLiteralArgument() { + if (templateContext != null) { + throw new IllegalStateException("cannot get literal argument if a template context exists"); + } + return args[0]; + } +} \ No newline at end of file diff --git a/api/src/main/java/com/google/common/flogger/EventAggregator.java b/api/src/main/java/com/google/common/flogger/EventAggregator.java new file mode 100644 index 00000000..403d3cb1 --- /dev/null +++ b/api/src/main/java/com/google/common/flogger/EventAggregator.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2020 The Flogger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.common.flogger; + +import com.google.common.flogger.util.Checks; +import com.google.errorprone.annotations.CheckReturnValue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * EventAggregator is used to log many same type events. The typical scenario is api request and response. + *

+ * If we log each api request and response, the log data will be too large. But if we do not log each api request and + * response, when there are some errors, we can not confirm if we get the request or return the response. + *

+ * EventAggregator can aggregate many same type events and log the key information within one log. + * For example: + * requestId=1053:200 | requestId=1054:200 | requestId=1055:500 | requestId=1056:200 | requestId=1057:200 | + * requestId=1063:200 | requestId=1064:200 | requestId=1065:200 | requestId=1066:404 | requestId=1067:200 | + *

+ * The best practice is to use {@link EventAggregator} to log brief information for all api request and response, + * and use {@link FluentLogger} to log detailed information for error requests and responses. + */ +@CheckReturnValue +public class EventAggregator extends AggregatedLogContext { + + /** + * Use LinkedBlockingQueue to store events. + *

+ * Two reasons for using LinkedBlockingQueue: + * 1. Thread-safe: many threads will use the same {@link EventAggregator} to log same type events. + * 2. Async log: logging aggregated events is a time-consuming action. It's better to use separate thread to do it. + */ + protected final BlockingQueue eventList; + + /** + * Instantiates a new Event aggregator. + * + * @param name the name + * @param logger the logger (see {@link FluentAggregatedLogger}). + * @param logSite the log site (see {@link LogSite}). + * @param pool the executor service pool used to periodically log data. + */ + EventAggregator(String name, FluentAggregatedLogger logger, LogSite logSite, + ScheduledExecutorService pool, int capacity) { + super(name, logger, logSite, pool); + eventList = new LinkedBlockingQueue(capacity); + } + + @Override + protected EventAggregator self() { + return this; + } + + /** + * Add event to {@link EventAggregator}. + * + * @param event the event + * @param content the content + */ + public void add(String event, String content) { + //try 3 times + int i = 0; + while (i++ < 3) { + try { + if (eventList.offer(new EventPair(event, content), 1, TimeUnit.MILLISECONDS)) { + if (shouldFlushByNumber()) { + asyncFlush(getNumberWindow()); + } + + break; + } else { + //If BlockingQueue is full, just immediately flush + asyncFlush(0); + Thread.sleep(1); + } + } catch (InterruptedException e) { + if (i == 2) { + //Do not log anything, just print stacktrace + e.printStackTrace(); + } + } + ; + } + ; + } + + @Override + public boolean shouldFlushByNumber() { + return eventList.size() >= getNumberWindow(); + } + + @Override + public int haveData() { + return eventList.size(); + } + + @Override + public String message(int count) { + Checks.checkArgument(count >= 0, "count should be >=0"); + + List eventBuffer = new ArrayList(); + if (count == 0) { + eventList.drainTo(eventBuffer); + } else { + eventList.drainTo(eventBuffer, count); + } + + return formatMessage(eventBuffer); + } + + private String formatMessage(List eventBuffer) { + int bufferSize = eventBuffer.size(); + StringBuilder builder = new StringBuilder(); + + builder.append(name).append("\n"); + for (int i = 0; i < bufferSize; i++) { + builder.append(eventBuffer.get(i).getKey()).append(":") + .append(eventBuffer.get(i).getValue()).append(" | "); + if ((i + 1) % 10 == 0) { + builder.append("\n"); + } + } + if (bufferSize % 10 != 0) { + builder.append("\n"); + } + + builder.append("\ntotal: ").append(bufferSize); + + return builder.toString(); + } + + /** + * Simple Event pair. + */ + static final class EventPair { + private final String key; + private final String value; + + /** + * Instantiates a new Event pair. + * + * @param key the key + * @param value the value + */ + public EventPair(String key, String value) { + this.key = key; + this.value = value; + } + + /** + * Gets key. + * + * @return the key + */ + public String getKey() { + return key; + } + + /** + * Gets value. + * + * @return the value + */ + public String getValue() { + return value; + } + } +} diff --git a/api/src/main/java/com/google/common/flogger/FluentAggregatedLogger.java b/api/src/main/java/com/google/common/flogger/FluentAggregatedLogger.java new file mode 100644 index 00000000..48992867 --- /dev/null +++ b/api/src/main/java/com/google/common/flogger/FluentAggregatedLogger.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2020 The Flogger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.common.flogger; + +import com.google.common.flogger.backend.LoggerBackend; +import com.google.common.flogger.backend.Platform; +import com.google.errorprone.annotations.CheckReturnValue; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import static com.google.common.flogger.util.Checks.checkNotNull; + +/** + * The default aggregated logger which uses the default parser and system configured backend. + *

+ * FluentAggregatedLogger will initiate {@link EventAggregator} or {@link StatAggregator}. + */ +@CheckReturnValue +public final class FluentAggregatedLogger extends AbstractLogger { + //Executor service pool for all aggregated loggers to periodically flush log. + private static final ScheduledExecutorService pool = Executors.newScheduledThreadPool(8); + + //Cache the AggregatedLogContext instance for multi threads + private final Map aggregatorMap = new ConcurrentHashMap(); + + //Visible for unit testing + protected FluentAggregatedLogger(LoggerBackend backend) { + super(backend); + } + + public static FluentAggregatedLogger forEnclosingClass() { + // NOTE: It is _vital_ that the call to "caller finder" is made directly inside the static + // factory method. See getCallerFinder() for more information. + String loggingClass = Platform.getCallerFinder().findLoggingClass(FluentAggregatedLogger.class); + return new FluentAggregatedLogger(Platform.getBackend(loggingClass)); + } + + /** + * Get EventAggregator + * + * @param name aggregator logger name, should be unique in the same FluentAggregatedLogger scope. + * @return EventAggregator + */ + public EventAggregator getEvent(String name) { + AggregatedLogContext aggregator = aggregatorMap.get(name); + if (aggregator == null) { + //Get logsite here for async write data in new Thread + LogSite logSite = checkNotNull(Platform.getCallerFinder().findLogSite(FluentAggregatedLogger.class, 0), + "logger backend must not return a null LogSite"); + aggregator = new EventAggregator(name, this, logSite, pool, 1024 * 1024); + aggregatorMap.putIfAbsent(name, aggregator); + + AggregatedLogContext old = aggregatorMap.putIfAbsent(name, aggregator); + + if (old != null) { + aggregator = old; + } + } + + if (!(aggregator instanceof EventAggregator)) { + throw new RuntimeException("There is another kind of logger with the same name"); + } + + return (EventAggregator) aggregator; + } + + /** + * Get StatAggregator + * + * @param name aggregator logger name, should be unique in the same FluentAggregatedLogger scope. + * @return StatAggregator + */ + public StatAggregator getStat(String name) { + AggregatedLogContext aggregator = aggregatorMap.get(name); + if (aggregator == null) { + //Get logsite here for async write data in new Thread + LogSite logSite = checkNotNull(Platform.getCallerFinder().findLogSite(FluentAggregatedLogger.class, 0), + "logger backend must not return a null LogSite"); + aggregator = new StatAggregator(name, this, logSite, pool, 1024 * 1024); + AggregatedLogContext old = aggregatorMap.putIfAbsent(name, aggregator); + + if (old != null) { + aggregator = old; + } + } + + if (!(aggregator instanceof StatAggregator)) { + throw new RuntimeException("There is another kind of logger with the same name: " + name); + } + + return (StatAggregator) aggregator; + } +} diff --git a/api/src/main/java/com/google/common/flogger/FluentLogger.java b/api/src/main/java/com/google/common/flogger/FluentLogger.java index bd1a8f33..209327d7 100644 --- a/api/src/main/java/com/google/common/flogger/FluentLogger.java +++ b/api/src/main/java/com/google/common/flogger/FluentLogger.java @@ -24,7 +24,7 @@ import java.util.logging.Level; /** - * The default implementation of {@link AbstractLogger} which returns the basic {@link LoggingApi} + * The default implementation of {@link AbstractMessageLogger} which returns the basic {@link LoggingApi} * and uses the default parser and system configured backend. *

* Note that when extending the logging API or specifying a new parser, you will need to create a @@ -36,7 +36,7 @@ * a specific logger implementation always get the same behavior. */ @CheckReturnValue -public final class FluentLogger extends AbstractLogger { +public final class FluentLogger extends AbstractMessageLogger { /** * The non-wildcard, fully specified, logging API for this logger. Fluent logger implementations * should specify a non-wildcard API like this with which to generify the abstract logger. diff --git a/api/src/main/java/com/google/common/flogger/LogContext.java b/api/src/main/java/com/google/common/flogger/LogContext.java index 0d5b3336..ff4e0675 100644 --- a/api/src/main/java/com/google/common/flogger/LogContext.java +++ b/api/src/main/java/com/google/common/flogger/LogContext.java @@ -41,7 +41,7 @@ * This class is an implementation of the base {@link LoggingApi} interface and acts as a holder for * any state applied to the log statement during the fluent call sequence. The lifecycle of a * logging context is very short; it is created by a logger, usually in response to a call to the - * {@link AbstractLogger#at(Level)} method, and normally lasts only as long as the log statement. + * {@link AbstractMessageLogger#at(Level)} method, and normally lasts only as long as the log statement. *

* This class should not be visible to normal users of the logging API and it is only needed when * extending the API to add more functionality. In order to extend the logging API and add methods @@ -53,7 +53,7 @@ */ @CheckReturnValue public abstract class LogContext< - LOGGER extends AbstractLogger, API extends LoggingApi> + LOGGER extends AbstractMessageLogger, API extends LoggingApi> implements LoggingApi, LogData { /** @@ -148,125 +148,6 @@ public void emit(Tags tags, KeyValueHandler out) { MetadataKey.single("stack_size", StackSize.class); } - static final class MutableMetadata extends Metadata { - /** - * The default number of key/value pairs we initially allocate space for when someone adds - * metadata to this context. - *

- * Note: As of 10/12 the VM allocates small object arrays very linearly with respect to the - * number of elements (an array has a 12 byte header with 4 bytes/element for object - * references). The allocation size is always rounded up to the next 8 bytes which means we - * can just pick a small value for the initial size and grow from there without too much waste. - *

- * For 4 key/value pairs, we will need 8 elements in the array, which will take up 48 bytes - * {@code (12 + (8 * 4) = 44}, which when rounded up is 48. - */ - private static final int INITIAL_KEY_VALUE_CAPACITY = 4; - - /** - * The array of key/value pairs to hold any metadata the might be added by the logger or any of - * the fluent methods on our API. This is an array so it is as space efficient as possible. - */ - private Object[] keyValuePairs = new Object[2 * INITIAL_KEY_VALUE_CAPACITY]; - /** The number of key/value pairs currently stored in the array. */ - private int keyValueCount = 0; - - @Override - public int size() { - return keyValueCount; - } - - @Override - public MetadataKey getKey(int n) { - if (n >= keyValueCount) { - throw new IndexOutOfBoundsException(); - } - return (MetadataKey) keyValuePairs[2 * n]; - } - - @Override - public Object getValue(int n) { - if (n >= keyValueCount) { - throw new IndexOutOfBoundsException(); - } - return keyValuePairs[(2 * n) + 1]; - } - - private int indexOf(MetadataKey key) { - for (int index = 0; index < keyValueCount; index++) { - if (keyValuePairs[2 * index].equals(key)) { - return index; - } - } - return -1; - } - - @Override - @NullableDecl - public T findValue(MetadataKey key) { - int index = indexOf(key); - return index != -1 ? key.cast(keyValuePairs[(2 * index) + 1]) : null; - } - - /** - * Adds the key/value pair to the metadata (growing the internal array as necessary). If the - * key cannot be repeated, and there is already a value for the key in the metadata, then the - * existing value is replaced, otherwise the value is added at the end of the metadata. - */ - void addValue(MetadataKey key, T value) { - if (!key.canRepeat()) { - int index = indexOf(key); - if (index != -1) { - keyValuePairs[(2 * index) + 1] = checkNotNull(value, "metadata value"); - return; - } - } - // Check that the array is big enough for one more element. - if (2 * (keyValueCount + 1) > keyValuePairs.length) { - // Use doubling here (this code should almost never be hit in normal usage and the total - // number of items should always stay relatively small. If this resizing algorithm is ever - // modified it is vital that the new value is always an even number. - keyValuePairs = Arrays.copyOf(keyValuePairs, 2 * keyValuePairs.length); - } - keyValuePairs[2 * keyValueCount] = checkNotNull(key, "metadata key"); - keyValuePairs[(2 * keyValueCount) + 1] = checkNotNull(value, "metadata value"); - keyValueCount += 1; - } - - /** Removes all key/value pairs for a given key. */ - void removeAllValues(MetadataKey key) { - int index = indexOf(key); - if (index >= 0) { - int dest = 2 * index; - int src = dest + 2; - while (src < (2 * keyValueCount)) { - Object nextKey = keyValuePairs[src]; - if (!nextKey.equals(key)) { - keyValuePairs[dest] = nextKey; - keyValuePairs[dest + 1] = keyValuePairs[src + 1]; - dest += 2; - } - src += 2; - } - // We know src & dest are +ve and (src > dest), so shifting is safe here. - keyValueCount -= (src - dest) >> 1; - while (dest < src) { - keyValuePairs[dest++] = null; - } - } - } - - /** Strictly for debugging. */ - @Override - public String toString() { - StringBuilder out = new StringBuilder("Metadata{"); - for (int n = 0; n < size(); n++) { - out.append(" '").append(getKey(n)).append("': ").append(getValue(n)); - } - return out.append(" }").toString(); - } - } - /** * A simple token used to identify cases where a single literal value is logged. Note that this * instance must be unique and it is important not to replace this with {@code ""} or any other diff --git a/api/src/main/java/com/google/common/flogger/MutableMetadata.java b/api/src/main/java/com/google/common/flogger/MutableMetadata.java new file mode 100644 index 00000000..02775393 --- /dev/null +++ b/api/src/main/java/com/google/common/flogger/MutableMetadata.java @@ -0,0 +1,133 @@ +package com.google.common.flogger; + +import com.google.common.flogger.backend.Metadata; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; + +import java.util.Arrays; + +import static com.google.common.flogger.util.Checks.checkNotNull; + +/** + * @Desctiption + * @Author wallace + * @Date 2020/12/26 + */ +public class MutableMetadata extends Metadata { + + /** + * The default number of key/value pairs we initially allocate space for when someone adds + * metadata to this context. + *

+ * Note: As of 10/12 the VM allocates small object arrays very linearly with respect to the + * number of elements (an array has a 12 byte header with 4 bytes/element for object + * references). The allocation size is always rounded up to the next 8 bytes which means we + * can just pick a small value for the initial size and grow from there without too much waste. + *

+ * For 4 key/value pairs, we will need 8 elements in the array, which will take up 48 bytes + * {@code (12 + (8 * 4) = 44}, which when rounded up is 48. + */ + private static final int INITIAL_KEY_VALUE_CAPACITY = 4; + + /** + * The array of key/value pairs to hold any metadata the might be added by the logger or any of + * the fluent methods on our API. This is an array so it is as space efficient as possible. + */ + private Object[] keyValuePairs = new Object[2 * INITIAL_KEY_VALUE_CAPACITY]; + /** The number of key/value pairs currently stored in the array. */ + private int keyValueCount = 0; + + @Override + public int size() { + return keyValueCount; + } + + @Override + public MetadataKey getKey(int n) { + if (n >= keyValueCount) { + throw new IndexOutOfBoundsException(); + } + return (MetadataKey) keyValuePairs[2 * n]; + } + + @Override + public Object getValue(int n) { + if (n >= keyValueCount) { + throw new IndexOutOfBoundsException(); + } + return keyValuePairs[(2 * n) + 1]; + } + + private int indexOf(MetadataKey key) { + for (int index = 0; index < keyValueCount; index++) { + if (keyValuePairs[2 * index].equals(key)) { + return index; + } + } + return -1; + } + + @Override + @NullableDecl + public T findValue(MetadataKey key) { + int index = indexOf(key); + return index != -1 ? key.cast(keyValuePairs[(2 * index) + 1]) : null; + } + + /** + * Adds the key/value pair to the metadata (growing the internal array as necessary). If the + * key cannot be repeated, and there is already a value for the key in the metadata, then the + * existing value is replaced, otherwise the value is added at the end of the metadata. + */ + void addValue(MetadataKey key, T value) { + if (!key.canRepeat()) { + int index = indexOf(key); + if (index != -1) { + keyValuePairs[(2 * index) + 1] = checkNotNull(value, "metadata value"); + return; + } + } + // Check that the array is big enough for one more element. + if (2 * (keyValueCount + 1) > keyValuePairs.length) { + // Use doubling here (this code should almost never be hit in normal usage and the total + // number of items should always stay relatively small. If this resizing algorithm is ever + // modified it is vital that the new value is always an even number. + keyValuePairs = Arrays.copyOf(keyValuePairs, 2 * keyValuePairs.length); + } + keyValuePairs[2 * keyValueCount] = checkNotNull(key, "metadata key"); + keyValuePairs[(2 * keyValueCount) + 1] = checkNotNull(value, "metadata value"); + keyValueCount += 1; + } + + /** Removes all key/value pairs for a given key. */ + void removeAllValues(MetadataKey key) { + int index = indexOf(key); + if (index >= 0) { + int dest = 2 * index; + int src = dest + 2; + while (src < (2 * keyValueCount)) { + Object nextKey = keyValuePairs[src]; + if (!nextKey.equals(key)) { + keyValuePairs[dest] = nextKey; + keyValuePairs[dest + 1] = keyValuePairs[src + 1]; + dest += 2; + } + src += 2; + } + // We know src & dest are +ve and (src > dest), so shifting is safe here. + keyValueCount -= (src - dest) >> 1; + while (dest < src) { + keyValuePairs[dest++] = null; + } + } + } + + /** Strictly for debugging. */ + @Override + public String toString() { + StringBuilder out = new StringBuilder("Metadata{"); + for (int n = 0; n < size(); n++) { + out.append(" '").append(getKey(n)).append("': ").append(getValue(n)); + } + return out.append(" }").toString(); + } +} diff --git a/api/src/main/java/com/google/common/flogger/StatAggregator.java b/api/src/main/java/com/google/common/flogger/StatAggregator.java new file mode 100644 index 00000000..9061578a --- /dev/null +++ b/api/src/main/java/com/google/common/flogger/StatAggregator.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2020 The Flogger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.common.flogger; + +import com.google.common.flogger.util.Checks; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * StatAggregator is used to log many same type performance data. The typical scenario is API process. + *

+ * StatAggregator can aggregate many same type performance data and simply calc the min/max/total/avg value. + * For example: + * API: get-user-api + * min:60, max:87, total:735, count:10, avg:73.5. + * [CONTEXT number_window=10 sample_rate=3 unit="ms" ] + *

+ * You can combine {@link EventAggregator} and {@link StatAggregator} and {@link FluentLogger} + * to log full information for API or other kind of event like this: + * - use {@link EventAggregator} to log key information like request id and response code. + * - use {@link StatAggregator} to log performance information + * - use {@link FluentLogger} to log detailed information for error requests or responses. + */ +public class StatAggregator extends AggregatedLogContext { + + /** + * Use LinkedBlockingQueue to store values. + *

+ * Two reasons for using LinkedBlockingQueue: + * 1. Thread-safe: many threads will use the same {@link StatAggregator} to log same type values. + * 2. Async log: logging aggregated value is a time-consuming action. It's better to use separate thread to do it. + */ + protected final BlockingQueue valueList; + private final AtomicInteger sampleCounter = new AtomicInteger(1); + //Only calc some data + private volatile int sampleRate = 1; //Calc all data by default. + + protected StatAggregator(String name, FluentAggregatedLogger logger, LogSite logSite, + ScheduledExecutorService pool, int capacity) { + super(name, logger, logSite, pool); + valueList = new LinkedBlockingQueue(capacity); + } + + @Override + protected StatAggregator self() { + return this; + } + + /** + * Set sample rate + * + * @param sampleRate + * @return + */ + public StatAggregator withSampleRate(int sampleRate) { + Checks.checkArgument(sampleRate > 0 && sampleRate <= 1000, + "Sample rate range should be (0,1000]"); + + this.sampleRate = sampleRate; + metadata.addValue(Key.SAMPLE_RATE, sampleRate); //Just for log context + + return self(); + } + + public int getSampleRate() { + return sampleRate; + } + + /** + * Set unit for log context. + * + * @param unit + * @return + */ + public StatAggregator withUnit(String unit) { + metadata.addValue(Key.UNIT_STRING, unit); + + return self(); + } + + public String getUnit() { + String unit = metadata.findValue(Key.UNIT_STRING); + return unit; + } + + /** + * Add value + * + * @param value + */ + public void add(long value) { + if (!sample()) { + return; + } + + //try 3 times + int i = 0; + while (i++ < 3) { + try { + if (valueList.offer(value, 1, TimeUnit.MILLISECONDS)) { + if (shouldFlushByNumber()) { + asyncFlush(getNumberWindow()); + } + + break; + } else { + //If BlockingQueue is full, just immediately flush + asyncFlush(0); + Thread.sleep(1); + } + } catch (InterruptedException e) { + if (i == 2) { + //Do not log anything, just print stacktrace + e.printStackTrace(); + } + } + ; + } + ; + } + + @Override + public boolean shouldFlushByNumber() { + return valueList.size() >= getNumberWindow(); + } + + @Override + public int haveData() { + return valueList.size(); + } + + @Override + public String message(int count) { + Checks.checkArgument(count >= 0, "count should be larger than 0"); + + List valueBuffer = new ArrayList(); + if (count == 0) { + valueList.drainTo(valueBuffer); + } else { + valueList.drainTo(valueBuffer, count); + } + + return formatMessage(valueBuffer); + } + + /** + * Sample data + * + * @return true: log; false: skip + */ + protected boolean sample() { + return sampleRate == 1 || (sampleCounter.getAndIncrement() % sampleRate == 0); + } + + private String formatMessage(List valueBuffer) { + + long min = Long.MAX_VALUE; + long max = Long.MIN_VALUE; + long total = 0; + double avg = 0; + + for (Long e : valueBuffer) { + if (e > max) { + max = e; + } + + if (e < min) { + min = e; + } + + total += e; + } + + StringBuilder builder = new StringBuilder(); + builder.append(name).append("\n"); + + if (!valueBuffer.isEmpty()) { + avg = Double.valueOf(total) / valueBuffer.size(); + + String sep = ", "; + builder.append("min:").append(min).append(sep) + .append("max:").append(max).append(sep) + .append("total:").append(total).append(sep) + .append("count:").append(valueBuffer.size()).append(sep) + .append("avg:").append(avg).append("."); + } else { + builder.append(" "); + } + + return builder.toString(); + } + + public static final class Key { + public static final MetadataKey SAMPLE_RATE = + MetadataKey.single("sample_rate", Integer.class); + public static final MetadataKey UNIT_STRING = + MetadataKey.single("unit", String.class); + + private Key() { + } + } +} diff --git a/api/src/main/java/com/google/common/flogger/backend/LoggerBackend.java b/api/src/main/java/com/google/common/flogger/backend/LoggerBackend.java index 688e9b90..c48c711a 100644 --- a/api/src/main/java/com/google/common/flogger/backend/LoggerBackend.java +++ b/api/src/main/java/com/google/common/flogger/backend/LoggerBackend.java @@ -22,7 +22,7 @@ * Interface for all logger backends. *

*

Implementation Notes:

- * Often each {@link com.google.common.flogger.AbstractLogger} instance will be instantiated with a + * Often each {@link com.google.common.flogger.AbstractMessageLogger} instance will be instantiated with a * new logger backend (to permit per-class logging behavior). Because of this it is important that * LoggerBackends have as little per-instance state as possible. */ diff --git a/api/src/main/java/com/google/common/flogger/backend/Platform.java b/api/src/main/java/com/google/common/flogger/backend/Platform.java index 635ae3fb..b1bcc7b0 100644 --- a/api/src/main/java/com/google/common/flogger/backend/Platform.java +++ b/api/src/main/java/com/google/common/flogger/backend/Platform.java @@ -18,7 +18,6 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; -import com.google.common.flogger.AbstractLogger; import com.google.common.flogger.LogSite; import com.google.common.flogger.context.ContextDataProvider; import com.google.common.flogger.context.Tags; @@ -139,7 +138,8 @@ public abstract static class LogCallerFinder { * @throws IllegalStateException if there was no caller of the specified logged passed on the * stack (which may occur if the logger class was invoked directly by JNI). */ - public abstract String findLoggingClass(Class> loggerClass); + //public abstract String findLoggingClass(Class> loggerClass); + public abstract String findLoggingClass(Class loggerClass); /** * Returns a LogSite found from the current stack trace for the caller of the log() method on diff --git a/api/src/main/java/com/google/common/flogger/backend/system/StackBasedCallerFinder.java b/api/src/main/java/com/google/common/flogger/backend/system/StackBasedCallerFinder.java index e4dd55b9..7947198c 100644 --- a/api/src/main/java/com/google/common/flogger/backend/system/StackBasedCallerFinder.java +++ b/api/src/main/java/com/google/common/flogger/backend/system/StackBasedCallerFinder.java @@ -16,7 +16,6 @@ package com.google.common.flogger.backend.system; -import com.google.common.flogger.AbstractLogger; import com.google.common.flogger.LogSite; import com.google.common.flogger.backend.Platform.LogCallerFinder; import com.google.common.flogger.util.CallerFinder; @@ -36,7 +35,7 @@ public static LogCallerFinder getInstance() { } @Override - public String findLoggingClass(Class> loggerClass) { + public String findLoggingClass(Class loggerClass) { // We can skip at most only 1 method from the analysis, the inferLoggingClass() method itself. StackTraceElement caller = CallerFinder.findCallerOf(loggerClass, new Throwable(), 1); if (caller != null) { diff --git a/api/src/main/java/com/google/common/flogger/example/FluentAggregatedLoggerExample.java b/api/src/main/java/com/google/common/flogger/example/FluentAggregatedLoggerExample.java new file mode 100644 index 00000000..d8287bc9 --- /dev/null +++ b/api/src/main/java/com/google/common/flogger/example/FluentAggregatedLoggerExample.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2012 The Flogger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.common.flogger.example; + +import com.google.common.flogger.EventAggregator; +import com.google.common.flogger.FluentAggregatedLogger; +import com.google.common.flogger.FluentLogger; +import com.google.common.flogger.StatAggregator; + +import java.util.logging.Level; + +/** + * FluentAggregatedLogger Example. + * You can run this example directly from Intellij IDE by choose "fluent_aggregated_logger_example" target. + */ +public class FluentAggregatedLoggerExample { + final static FluentLogger logger1 = FluentLogger.forEnclosingClass(); + final static FluentAggregatedLogger logger2 = FluentAggregatedLogger.forEnclosingClass(); + + public static void main(String[] args) throws InterruptedException { + logger1.at(Level.INFO).log("hello, world"); + + EventAggregator eventAggregator = logger2.getEvent("get-user-api-resp").withTimeWindow(5).withNumberWindow(20).start(); + for(int i = 0; i < 92; i++) { + eventAggregator.add("requestId=10" + i, "200"); + } + + StatAggregator statAggregator = logger2.getStat("get-user-api-perf") + .withNumberWindow(10).withSampleRate(3).withUnit("ms"); + for(int i = 0; i < 100; i++) { + statAggregator.add(i); + } + } +} diff --git a/api/src/test/java/com/google/common/flogger/AggregatedLogContextTest.java b/api/src/test/java/com/google/common/flogger/AggregatedLogContextTest.java new file mode 100644 index 00000000..845a32a0 --- /dev/null +++ b/api/src/test/java/com/google/common/flogger/AggregatedLogContextTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2020 The Flogger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.common.flogger; + +import com.google.common.flogger.testing.FakeLoggerBackend; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +@RunWith(JUnit4.class) +public class AggregatedLogContextTest { + final static FluentAggregatedLogger logger2 = FluentAggregatedLogger.forEnclosingClass(); + + @Test + public void testGetName() { + String name = "test"; + FakeLoggerBackend backend = new FakeLoggerBackend(); + FluentAggregatedLogger logger = new FluentAggregatedLogger(backend); + EventAggregator eventAggregator = logger.getEvent(name); + + assertThat(eventAggregator.getName()).isEqualTo(name); + } + + @Test + public void testWithTimeWindow() { + FakeLoggerBackend backend = new FakeLoggerBackend(); + FluentAggregatedLogger logger = new FluentAggregatedLogger(backend); + EventAggregator eventAggregator = logger.getEvent("test"); + + // Test lower bound + try { + eventAggregator.withTimeWindow(-1); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + try { + eventAggregator.withTimeWindow(0); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + eventAggregator.withTimeWindow(1).add("timewindow", "1"); + eventAggregator.flush(0); + backend.assertLogged(0).metadata().containsUniqueEntry(AggregatedLogContext.Key.TIME_WINDOW, 1); + + // Test upper bound + try { + eventAggregator.withTimeWindow(3601); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + + } + try { + eventAggregator.withTimeWindow(10000); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + + EventAggregator eventAggregator2 = logger.getEvent("test2"); + eventAggregator2.withTimeWindow(3600).add("timewindow", "3600"); + eventAggregator2.flush(0); + backend.assertLogged(1).metadata().containsUniqueEntry(AggregatedLogContext.Key.TIME_WINDOW, 3600); + + // Test repeatedly set + eventAggregator.withTimeWindow(2).add("timewindow", "2"); + eventAggregator.flush(0); + backend.assertLogged(2).metadata().containsUniqueEntry(AggregatedLogContext.Key.TIME_WINDOW, 2); + try { + eventAggregator = eventAggregator.start().withTimeWindow(3); + fail("expected RuntimeException"); + } catch (RuntimeException e) { + } + } + + @Test + public void testWithNumberWindow() { + FakeLoggerBackend backend = new FakeLoggerBackend(); + FluentAggregatedLogger logger = new FluentAggregatedLogger(backend); + EventAggregator eventAggregator = logger.getEvent("test"); + + // Test lower bound + try { + eventAggregator.withNumberWindow(-1); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + try { + eventAggregator.withNumberWindow(0); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + eventAggregator.withNumberWindow(1).add("numberwindow", "1"); + eventAggregator.flush(0); + backend.assertLogged(0).metadata().containsUniqueEntry(AggregatedLogContext.Key.NUMBER_WINDOW, 1); + + // Test upper bound + try { + eventAggregator.withNumberWindow(1000 * 1000 + 1); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + + } + try { + eventAggregator.withNumberWindow(1024 * 1024); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + eventAggregator.withNumberWindow(1000 * 1000).add("timewindow", "1000 * 1000"); + eventAggregator.flush(0); + backend.assertLogged(1).metadata().containsUniqueEntry(AggregatedLogContext.Key.NUMBER_WINDOW, 1000 * 1000); + } + + @Test + public void testShouldFlush() throws InterruptedException { + FakeLoggerBackend backend = new FakeLoggerBackend(); + FluentAggregatedLogger logger = new FluentAggregatedLogger(backend); + EventAggregator eventAggregator = logger.getEvent("test"); + + assertThat(eventAggregator.shouldFlushByNumber()).isFalse(); + + eventAggregator = eventAggregator.withNumberWindow(10); + for (int i = 0; i < 9; i++) { + eventAggregator.add("hello", "world"); + } + assertThat(eventAggregator.shouldFlushByNumber()).isFalse(); + eventAggregator.add("hello", "world"); + assertThat(eventAggregator.shouldFlushByNumber()).isTrue(); + + Thread.sleep(10); // Waiting for async flush thread to finish + eventAggregator.add("hello", "world"); + assertThat(eventAggregator.shouldFlushByNumber()).isFalse(); + } +} diff --git a/api/src/test/java/com/google/common/flogger/EventAggregatorTest.java b/api/src/test/java/com/google/common/flogger/EventAggregatorTest.java new file mode 100644 index 00000000..313e785b --- /dev/null +++ b/api/src/test/java/com/google/common/flogger/EventAggregatorTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2021 The Flogger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.common.flogger; + +import com.google.common.flogger.backend.Platform; +import com.google.common.flogger.testing.FakeLoggerBackend; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import static com.google.common.flogger.util.Checks.checkNotNull; +import static com.google.common.truth.Truth.assertThat; + +@RunWith(JUnit4.class) +public class EventAggregatorTest { + + private EventAggregator create(String name, int capacity, FakeLoggerBackend backend) { + FluentAggregatedLogger logger = new FluentAggregatedLogger(backend); + LogSite logSite = checkNotNull(Platform.getCallerFinder().findLogSite(FluentAggregatedLogger.class, 0), + "logger backend must not return a null LogSite"); + ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); + + return new EventAggregator(name, logger, logSite, pool, capacity); + } + + @Test + public void testAdd() throws InterruptedException { + String name = "test"; + FakeLoggerBackend backend = new FakeLoggerBackend(); + EventAggregator eventAggregator = create(name, 2, backend); + + assertThat(eventAggregator.getName()).isEqualTo(name); + + eventAggregator.add("event1", "hello"); + eventAggregator.add("event2", "world"); + eventAggregator.add("event3", "flogger"); + + Thread.sleep(3); //wait for async flush thread to finish + eventAggregator.flush(0); + + String message1 = "test\n" + + "event1:hello | event2:world | \n" + + "\n" + + "total: 2"; + backend.assertLogged(0).hasMessage(message1); + + String message2 = "test\n" + + "event3:flogger | \n" + + "\n" + + "total: 1"; + backend.assertLogged(1).hasMessage(message2); + } + + @Test + public void testHaveData() { + String name = "test"; + FakeLoggerBackend backend = new FakeLoggerBackend(); + EventAggregator eventAggregator = create(name, 2, backend); + + eventAggregator.add("event1", "hello"); + assertThat(eventAggregator.haveData()).isEqualTo(1); + + eventAggregator.add("event2", "world"); + assertThat(eventAggregator.haveData()).isEqualTo(2); + + eventAggregator.add("event3", "flogger"); + assertThat(eventAggregator.haveData()).isEqualTo(1); + //Assert.assertEquals(eventAggregator.haveData(),3); + + eventAggregator.flush(0); + assertThat(eventAggregator.haveData()).isEqualTo(0); + + } +} diff --git a/api/src/test/java/com/google/common/flogger/FluentAggregatedLoggerTest.java b/api/src/test/java/com/google/common/flogger/FluentAggregatedLoggerTest.java new file mode 100644 index 00000000..7972d670 --- /dev/null +++ b/api/src/test/java/com/google/common/flogger/FluentAggregatedLoggerTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 The Flogger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.common.flogger; + +import com.google.common.flogger.testing.FakeLoggerBackend; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.logging.Level; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +/** + * FluentAggregatedLogger is typically very simple classes whose only real responsibility is as a factory + * for a specific API implementation. As such it needs very few tests itself. + *

+ * See AggregatedLogContextTest.java for the vast majority of tests related to base logging behaviour. + */ +@RunWith(JUnit4.class) +public class FluentAggregatedLoggerTest { + @Test + public void testCreate() { + FluentAggregatedLogger logger = FluentAggregatedLogger.forEnclosingClass(); + assertThat(logger.getName()).isEqualTo(FluentAggregatedLoggerTest.class.getName()); + assertThat(logger.getBackend().getLoggerName()).isEqualTo(FluentAggregatedLoggerTest.class.getName()); + } + + @Test + public void testGetEvent() { + FakeLoggerBackend backend = new FakeLoggerBackend(); + FluentAggregatedLogger logger = new FluentAggregatedLogger(backend); + backend.setLevel(Level.INFO); + + // Check name + String name1 = "event1"; + EventAggregator eventAggregator = logger.getEvent(name1); + assertThat(eventAggregator.getName()).isEqualTo(name1); + // Test repeatedly get + EventAggregator same = logger.getEvent(name1); + assertThat(eventAggregator).isSameInstanceAs(same); + + //Test different name + String name2 = "event2"; + EventAggregator eventAggregator2 = logger.getEvent(name2); + assertThat(eventAggregator2.getName()).isEqualTo(name2); + assertThat(eventAggregator).isNotSameInstanceAs(eventAggregator2); + + //Test different type aggregator with the same name + try { + StatAggregator statAggregator = logger.getStat(name1); + fail("expected RuntimeException"); + } catch (RuntimeException e) { + + } + } + + @Test + public void testGetStat() { + FakeLoggerBackend backend = new FakeLoggerBackend(); + FluentAggregatedLogger logger = new FluentAggregatedLogger(backend); + backend.setLevel(Level.INFO); + + // Check name + String name1 = "stat1"; + StatAggregator statAggregator1 = logger.getStat(name1); + assertThat(statAggregator1.getName()).isEqualTo(name1); + + // Test repeatedly get + StatAggregator same = logger.getStat(name1); + assertThat(statAggregator1).isEqualTo(same); + + //Test different name + String name2 = "stat2"; + StatAggregator statAggregator2 = logger.getStat(name2); + assertThat(statAggregator2.getName()).isEqualTo(name2); + assertThat(statAggregator1).isNotSameInstanceAs(statAggregator2); + + //Test different type aggregator with the same name + try { + EventAggregator eventAggregator = logger.getEvent(name1); + fail("expected RuntimeException"); + } catch (RuntimeException e) { + + } + } +} diff --git a/api/src/test/java/com/google/common/flogger/StatAggregatorTest.java b/api/src/test/java/com/google/common/flogger/StatAggregatorTest.java new file mode 100644 index 00000000..268510d5 --- /dev/null +++ b/api/src/test/java/com/google/common/flogger/StatAggregatorTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2020 The Flogger Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.common.flogger; + +import com.google.common.flogger.backend.Platform; +import com.google.common.flogger.testing.FakeLoggerBackend; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import static com.google.common.flogger.util.Checks.checkNotNull; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +@RunWith(JUnit4.class) +public class StatAggregatorTest { + + private StatAggregator create(String name, int capacity, FakeLoggerBackend backend) { + FluentAggregatedLogger logger = new FluentAggregatedLogger(backend); + LogSite logSite = checkNotNull(Platform.getCallerFinder().findLogSite(FluentAggregatedLogger.class, 0), + "logger backend must not return a null LogSite"); + ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); + + return new StatAggregator(name, logger, logSite, pool, capacity); + } + + @Test + public void testWithSampleRate() { + FakeLoggerBackend backend = new FakeLoggerBackend(); + StatAggregator statAggregator = create("test", 3, backend); + + // Test lower bound + try { + statAggregator = statAggregator.withSampleRate(-1); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + try { + statAggregator = statAggregator.withSampleRate(0); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + + statAggregator = statAggregator.withSampleRate(1); + statAggregator.add(10); + statAggregator.add(10); + assertThat(statAggregator.valueList.size()).isEqualTo(2); + + statAggregator.flush(0); //Clear value list + statAggregator = statAggregator.withSampleRate(2); + + statAggregator.add(10); + assertThat(statAggregator.valueList.size()).isEqualTo(0); + assertThat(statAggregator.valueList.size()).isEqualTo(0); + + statAggregator.add(10); + assertThat(statAggregator.valueList.size()).isEqualTo(1); + + statAggregator.add(10); + assertThat(statAggregator.valueList.size()).isEqualTo(1); + + statAggregator.add(10); + assertThat(statAggregator.valueList.size()).isEqualTo(2); + + // Test upper bound + statAggregator = statAggregator.withSampleRate(999).withSampleRate(1000); + assertThat(statAggregator.getSampleRate()).isEqualTo(1000); + + try { + statAggregator = statAggregator.withSampleRate(1001); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testWithUnit() { + FakeLoggerBackend backend = new FakeLoggerBackend(); + StatAggregator statAggregator = create("test", 3, backend); + + assertThat(statAggregator.getUnit()).isNull(); + + String unit = "ms"; + statAggregator = statAggregator.withUnit(unit); + assertThat(statAggregator.getUnit()).isEqualTo(unit); + + statAggregator.add(10); + statAggregator.flush(0); + + backend.assertLogged(0).metadata().containsUniqueEntry(StatAggregator.Key.UNIT_STRING, unit); + } + + @Test + public void testSample() { + FakeLoggerBackend backend = new FakeLoggerBackend(); + StatAggregator statAggregator = create("test", 3, backend); + + assertThat(statAggregator.sample()).isTrue(); + assertThat(statAggregator.sample()).isTrue(); + assertThat(statAggregator.sample()).isTrue(); + + statAggregator = statAggregator.withSampleRate(2); + assertThat(statAggregator.sample()).isFalse(); + assertThat(statAggregator.sample()).isTrue(); + assertThat(statAggregator.sample()).isFalse(); + assertThat(statAggregator.sample()).isTrue(); + assertThat(statAggregator.sample()).isFalse(); + } + + @Test + public void testHaveData() { + FakeLoggerBackend backend = new FakeLoggerBackend(); + StatAggregator statAggregator = create("test", 3, backend); + + assertThat(statAggregator.haveData()).isEqualTo(0); + statAggregator.add(10); + assertThat(statAggregator.haveData()).isEqualTo(1); + + statAggregator.flush(0); + assertThat(statAggregator.haveData()).isEqualTo(0); + } + + @Test + public void testAdd() throws InterruptedException { + String name = "test"; + FakeLoggerBackend backend = new FakeLoggerBackend(); + StatAggregator statAggregator = create(name, 4, backend); + + assertThat(statAggregator.getName()).isEqualTo(name); + + // Test value list is full + statAggregator = statAggregator.withNumberWindow(4); + for (int i = 0; i < 5; i++) { + statAggregator.add(10); + } + Thread.sleep(10); //wait for async flush finished. + String message = "test\n" + + "min:10, max:10, total:40, count:4, avg:10.0."; + backend.assertLogged(0).hasMessage(message); + + // Test flush based on number window + statAggregator = statAggregator.withNumberWindow(3); + statAggregator.add(10); + statAggregator.add(10); + Thread.sleep(10); // Wait for async flush finished. + + message = "test\n" + + "min:10, max:10, total:30, count:3, avg:10.0."; + backend.assertLogged(1).hasMessage(message); + } +} diff --git a/api/src/test/java/com/google/common/flogger/backend/system/SimpleBackendLoggerTest.java b/api/src/test/java/com/google/common/flogger/backend/system/SimpleBackendLoggerTest.java index 3586eea9..9b5844ea 100644 --- a/api/src/test/java/com/google/common/flogger/backend/system/SimpleBackendLoggerTest.java +++ b/api/src/test/java/com/google/common/flogger/backend/system/SimpleBackendLoggerTest.java @@ -169,7 +169,7 @@ public void testPrintfDateTime() { backend.log(withPrintfStyle("Seconds=%tS", cal.getTimeInMillis())); logger.assertLogCount(4); - logger.assertLogEntry(0, INFO, "Day=SAT 13, Month=July, Year=1985"); + logger.assertLogEntry(0, INFO, "Day=SAT 13, Month=Jul, Year=1985"); logger.assertLogEntry(1, INFO, "Time=5:20:03 AM"); logger.assertLogEntry(2, INFO, "Sat Jul 13 05:20:03 GMT 1985 "); // padded logger.assertLogEntry(3, INFO, "Seconds=03"); diff --git a/api/src/test/java/com/google/common/flogger/testing/TestLogger.java b/api/src/test/java/com/google/common/flogger/testing/TestLogger.java index fb18c273..36d4fd20 100644 --- a/api/src/test/java/com/google/common/flogger/testing/TestLogger.java +++ b/api/src/test/java/com/google/common/flogger/testing/TestLogger.java @@ -16,7 +16,7 @@ package com.google.common.flogger.testing; -import com.google.common.flogger.AbstractLogger; +import com.google.common.flogger.AbstractMessageLogger; import com.google.common.flogger.LogContext; import com.google.common.flogger.LoggingApi; import com.google.common.flogger.backend.LoggerBackend; @@ -31,7 +31,7 @@ * *

This class is mutable and not thread safe. */ -public final class TestLogger extends AbstractLogger { +public final class TestLogger extends AbstractMessageLogger { // Midnight Jan 1st, 2000 (GMT) private static final long DEFAULT_TIMESTAMP_NANOS = 946684800000000000L; diff --git a/google/src/main/java/com/google/common/flogger/GoogleLogContext.java b/google/src/main/java/com/google/common/flogger/GoogleLogContext.java index 24dca3ee..57b34aa5 100644 --- a/google/src/main/java/com/google/common/flogger/GoogleLogContext.java +++ b/google/src/main/java/com/google/common/flogger/GoogleLogContext.java @@ -31,7 +31,7 @@ */ @CheckReturnValue public abstract class GoogleLogContext< - LOGGER extends AbstractLogger, API extends GoogleLoggingApi> + LOGGER extends AbstractMessageLogger, API extends GoogleLoggingApi> extends LogContext implements GoogleLoggingApi { /** diff --git a/google/src/main/java/com/google/common/flogger/GoogleLogger.java b/google/src/main/java/com/google/common/flogger/GoogleLogger.java index eefaf676..2110389e 100644 --- a/google/src/main/java/com/google/common/flogger/GoogleLogger.java +++ b/google/src/main/java/com/google/common/flogger/GoogleLogger.java @@ -24,11 +24,11 @@ import java.util.logging.Level; /** - * The default Google specific implementation of {@link AbstractLogger} which extends the core + * The default Google specific implementation of {@link AbstractMessageLogger} which extends the core * {@link LoggingApi} to add Google specific functionality. */ @CheckReturnValue -public final class GoogleLogger extends AbstractLogger { +public final class GoogleLogger extends AbstractMessageLogger { /** See {@link GoogleLoggingApi}. */ public interface Api extends GoogleLoggingApi {}