blob: 4772bbfe97ddae6815b59ad747525cfdbfe21ad0 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* 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.android.server.utils;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.Log;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.SimpleDateFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
* Logs human-readable events for debugging purposes.
*/
public class EventLogger {
/** Prefix for the title added at the beginning of a {@link #dump(PrintWriter)} operation */
private static final String DUMP_TITLE_PREFIX = "Events log: ";
/** Identifies the source of events. */
@Nullable private final String mTag;
/** Stores the events using a ring buffer. */
private final ArrayDeque<Event> mEvents;
/**
* The maximum number of events to keep in {@code mEvents}.
*
* <p>Calling {@link #enqueue} when the size of {@link #mEvents} matches the threshold will
* cause the oldest event to be evicted.
*/
private final int mMemSize;
/**
* Constructor for logger.
* @param size the maximum number of events to keep in log
* @param tag the string displayed before the recorded log
*/
public EventLogger(int size, @Nullable String tag) {
mEvents = new ArrayDeque<>(size);
mMemSize = size;
mTag = tag;
}
/** Enqueues {@code event} to be logged. */
public synchronized void enqueue(Event event) {
if (mEvents.size() >= mMemSize) {
mEvents.removeFirst();
}
mEvents.addLast(event);
}
/**
* Add a string-based event to the log, and print it to logcat with a specific severity.
* @param msg the message for the logs
* @param logType the type of logcat entry
* @param tag the logcat tag to use
*/
public synchronized void enqueueAndLog(String msg, @Event.LogType int logType, String tag) {
final Event event = new StringEvent(msg);
enqueue(event.printLog(logType, tag));
}
/** Dumps events into the given {@link DumpSink}. */
public synchronized void dump(DumpSink dumpSink) {
dumpSink.sink(mTag, new ArrayList<>(mEvents));
}
/** Dumps events using {@link PrintWriter}. */
public synchronized void dump(PrintWriter pw) {
dump(pw, "" /* prefix */);
}
protected String getDumpTitle() {
if (mTag == null) {
return DUMP_TITLE_PREFIX;
}
return DUMP_TITLE_PREFIX + mTag;
}
/** Dumps events using {@link PrintWriter} with a certain indent. */
public synchronized void dump(PrintWriter pw, String indent) {
pw.println(getDumpTitle());
for (Event evt : mEvents) {
pw.println(indent + evt.toString());
}
}
/** Receives events from {@link EventLogger} upon a {@link #dump(DumpSink)} call. **/
public interface DumpSink {
/** Processes given events into some pipeline with a given tag. **/
void sink(String tag, List<Event> events);
}
public abstract static class Event {
/** Timestamps formatter. */
private static final SimpleDateFormat sFormat =
new SimpleDateFormat("MM-dd HH:mm:ss:SSS", Locale.US);
private final long mTimestamp;
public Event() {
mTimestamp = System.currentTimeMillis();
}
public String toString() {
return (new StringBuilder(sFormat.format(new Date(mTimestamp))))
.append(" ").append(eventToString()).toString();
}
/**
* Causes the string message for the event to appear in the logcat.
* Here is an example of how to create a new event (a StringEvent), adding it to the logger
* (an instance of AudioEventLogger) while also making it show in the logcat:
* <pre>
* myLogger.log(
* (new StringEvent("something for logcat and logger")).printLog(MyClass.TAG) );
* </pre>
* @param tag the tag for the android.util.Log.v
* @return the same instance of the event
*/
public Event printLog(String tag) {
return printLog(ALOGI, tag);
}
/** @hide */
@IntDef(flag = false, value = {
ALOGI,
ALOGE,
ALOGW,
ALOGV }
)
@Retention(RetentionPolicy.SOURCE)
public @interface LogType {}
public static final int ALOGI = 0;
public static final int ALOGE = 1;
public static final int ALOGW = 2;
public static final int ALOGV = 3;
/**
* Same as {@link #printLog(String)} with a log type
* @param type one of {@link #ALOGI}, {@link #ALOGE}, {@link #ALOGV}
* @param tag
* @return
*/
public Event printLog(@LogType int type, String tag) {
switch (type) {
case ALOGI:
Log.i(tag, eventToString());
break;
case ALOGE:
Log.e(tag, eventToString());
break;
case ALOGW:
Log.w(tag, eventToString());
break;
case ALOGV:
default:
Log.v(tag, eventToString());
break;
}
return this;
}
/**
* Convert event to String.
* This method is only called when the logger history is about to the dumped,
* so this method is where expensive String conversions should be made, not when the Event
* subclass is created.
* Timestamp information will be automatically added, do not include it.
* @return a string representation of the event that occurred.
*/
public abstract String eventToString();
}
public static class StringEvent extends Event {
@Nullable
private final String mSource;
private final String mDescription;
/** Creates event from {@code source} and formatted {@code description} with {@code args} */
public static StringEvent from(@NonNull String source,
@NonNull String description, Object... args) {
return new StringEvent(source, String.format(Locale.US, description, args));
}
public StringEvent(String description) {
this(null /* source */, description);
}
public StringEvent(String source, String description) {
mSource = source;
mDescription = description;
}
@Override
public String eventToString() {
if (mSource == null) {
return mDescription;
}
// [source ] optional description
return String.format("[%-40s] %s",
mSource,
(mDescription == null ? "" : mDescription));
}
}
}