New SysUi logger

Test: atest SysuiLogTest
Test: atest RichEventTest
Bug: 141470043
Change-Id: Ied8ccc58734490230039c851cd3f020a048642d8
This commit is contained in:
Beverly
2019-09-23 14:54:33 -04:00
parent 7ef9e2f949
commit c65c4087cb
5 changed files with 488 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2019 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.systemui.log;
import android.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Stores information about an event that occurred in SystemUI to be used for debugging and triage.
* Every event has a time stamp, log level and message.
* Events are stored in {@link SysuiLog} and can be printed in a dumpsys.
*/
public class Event {
public static final int UNINITIALIZED = -1;
@IntDef({ERROR, WARN, INFO, DEBUG, VERBOSE})
@Retention(RetentionPolicy.SOURCE)
public @interface Level {}
public static final int VERBOSE = 2;
public static final int DEBUG = 3;
public static final int INFO = 4;
public static final int WARN = 5;
public static final int ERROR = 6;
private long mTimestamp;
private @Level int mLogLevel = DEBUG;
protected String mMessage;
public Event(String message) {
mTimestamp = System.currentTimeMillis();
mMessage = message;
}
public Event(@Level int logLevel, String message) {
mTimestamp = System.currentTimeMillis();
mLogLevel = logLevel;
mMessage = message;
}
public String getMessage() {
return mMessage;
}
public long getTimestamp() {
return mTimestamp;
}
public @Level int getLogLevel() {
return mLogLevel;
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2019 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.systemui.log;
/**
* Stores information about an event that occurred in SystemUI to be used for debugging and triage.
* Every rich event has a time stamp, event type, and log level, with the option to provide the
* reason this event was triggered.
* Events are stored in {@link SysuiLog} and can be printed in a dumpsys.
*/
public abstract class RichEvent extends Event {
private final int mType;
private final String mReason;
/**
* Create a rich event that includes an event type that matches with an index in the array
* getEventLabels().
*/
public RichEvent(@Event.Level int logLevel, int type, String reason) {
super(logLevel, null);
final int numEvents = getEventLabels().length;
if (type < 0 || type >= numEvents) {
throw new IllegalArgumentException("Unsupported event type. Events only supported"
+ " from 0 to " + (numEvents - 1) + ", but given type=" + type);
}
mType = type;
mReason = reason;
mMessage = getEventLabels()[mType] + " " + mReason;
}
/**
* Returns an array of the event labels. The index represents the event type and the
* corresponding String stored at that index is the user-readable representation of that event.
* @return array of user readable events, where the index represents its event type constant
*/
public abstract String[] getEventLabels();
public int getType() {
return mType;
}
public String getReason() {
return mReason;
}
/**
* Builder to build a RichEvent.
* @param <B> Log specific builder that is extending this builder
*/
public abstract static class Builder<B extends Builder<B>> {
public static final int UNINITIALIZED = -1;
private B mBuilder = getBuilder();
protected int mType = UNINITIALIZED;
protected String mReason;
protected @Level int mLogLevel;
/**
* Get the log-specific builder.
*/
public abstract B getBuilder();
/**
* Build the log-specific event.
*/
public abstract RichEvent build();
/**
* Optional - set the log level. Defaults to DEBUG.
*/
public B setLogLevel(@Level int logLevel) {
mLogLevel = logLevel;
return mBuilder;
}
/**
* Required - set the event type. These events must correspond with the events from
* getEventLabels().
*/
public B setType(int type) {
mType = type;
return mBuilder;
}
/**
* Optional - set the reason why this event was triggered.
*/
public B setReason(String reason) {
mReason = reason;
return mBuilder;
}
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright (C) 2019 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.systemui.log;
import android.os.Build;
import android.os.SystemProperties;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.DumpController;
import com.android.systemui.Dumpable;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayDeque;
import java.util.Locale;
/**
* Thread-safe logger in SystemUI which prints logs to logcat and stores logs to be
* printed by the DumpController. This is an alternative to printing directly
* to avoid logs being deleted by chatty. The number of logs retained is varied based on
* whether the build is {@link Build.IS_DEBUGGABLE}.
*
* To manually view the logs via adb:
* adb shell dumpsys activity service com.android.systemui/.SystemUIService \
* dependency DumpController <SysuiLogId>
*/
public class SysuiLog implements Dumpable {
public static final SimpleDateFormat DATE_FORMAT =
new SimpleDateFormat("MM-dd HH:mm:ss", Locale.US);
private final Object mDataLock = new Object();
private final String mId;
private final int mMaxLogs;
private boolean mEnabled;
@VisibleForTesting protected ArrayDeque<Event> mTimeline;
/**
* Creates a SysuiLog
* To enable or disable logs, set the system property and then restart the device:
* adb shell setprop sysui.log.enabled.<id> true/false && adb reboot
* @param dumpController where to register this logger's dumpsys
* @param id user-readable tag for this logger
* @param maxDebugLogs maximum number of logs to retain when {@link sDebuggable} is true
* @param maxLogs maximum number of logs to retain when {@link sDebuggable} is false
*/
public SysuiLog(DumpController dumpController, String id, int maxDebugLogs, int maxLogs) {
this(dumpController, id, sDebuggable ? maxDebugLogs : maxLogs,
SystemProperties.getBoolean(SYSPROP_ENABLED_PREFIX + id, DEFAULT_ENABLED));
}
@VisibleForTesting
protected SysuiLog(DumpController dumpController, String id, int maxLogs, boolean enabled) {
mId = id;
mMaxLogs = maxLogs;
mEnabled = enabled;
mTimeline = mEnabled ? new ArrayDeque<>(mMaxLogs) : null;
dumpController.registerDumpable(mId, this);
}
public SysuiLog(DumpController dumpController, String id) {
this(dumpController, id, DEFAULT_MAX_DEBUG_LOGS, DEFAULT_MAX_LOGS);
}
/**
* Logs an event to the timeline which can be printed by the dumpsys.
* May also log to logcat if enabled.
* @return true if event was logged, else false
*/
public boolean log(Event event) {
if (!mEnabled) {
return false;
}
synchronized (mDataLock) {
if (mTimeline.size() >= mMaxLogs) {
mTimeline.removeFirst();
}
mTimeline.add(event);
}
if (LOG_TO_LOGCAT_ENABLED) {
final String strEvent = eventToString(event);
switch (event.getLogLevel()) {
case Event.VERBOSE:
Log.v(mId, strEvent);
break;
case Event.DEBUG:
Log.d(mId, strEvent);
break;
case Event.ERROR:
Log.e(mId, strEvent);
break;
case Event.INFO:
Log.i(mId, strEvent);
break;
case Event.WARN:
Log.w(mId, strEvent);
break;
}
}
return true;
}
/**
* @return user-readable string of the given event
*/
public String eventToString(Event event) {
StringBuilder sb = new StringBuilder();
sb.append(SysuiLog.DATE_FORMAT.format(event.getTimestamp()));
sb.append(" ");
sb.append(event.getMessage());
return sb.toString();
}
/**
* only call on this method if you have the mDataLock
*/
private void dumpTimelineLocked(PrintWriter pw) {
pw.println("\tTimeline:");
for (Event event : mTimeline) {
pw.println("\t" + eventToString(event));
}
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println(mId + ":");
if (mEnabled) {
synchronized (mDataLock) {
dumpTimelineLocked(pw);
}
} else {
pw.print(" - Logging disabled.");
}
}
private static boolean sDebuggable = Build.IS_DEBUGGABLE;
private static final String SYSPROP_ENABLED_PREFIX = "sysui.log.enabled.";
private static final boolean LOG_TO_LOGCAT_ENABLED = sDebuggable;
private static final boolean DEFAULT_ENABLED = sDebuggable;
private static final int DEFAULT_MAX_DEBUG_LOGS = 100;
private static final int DEFAULT_MAX_LOGS = 50;
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2019 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.systemui.log;
import static junit.framework.Assert.assertEquals;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import junit.framework.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class RichEventTest extends SysuiTestCase {
private static final int TOTAL_EVENT_TYPES = 1;
@Test
public void testCreateRichEvent_invalidType() {
try {
// indexing for events starts at 0, so TOTAL_EVENT_TYPES is an invalid type
new TestableRichEvent(Event.DEBUG, TOTAL_EVENT_TYPES, "msg");
} catch (IllegalArgumentException e) {
// expected
return;
}
Assert.fail("Expected an invalidArgumentException since the event type was invalid.");
}
@Test
public void testCreateRichEvent() {
final int eventType = 0;
RichEvent e = new TestableRichEvent(Event.DEBUG, eventType, "msg");
assertEquals(e.getType(), eventType);
}
class TestableRichEvent extends RichEvent {
TestableRichEvent(int logLevel, int type, String reason) {
super(logLevel, type, reason);
}
@Override
public String[] getEventLabels() {
return new String[]{"ACTION_NAME"};
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright (C) 2019 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.systemui.log;
import static junit.framework.Assert.assertEquals;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.DumpController;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class SysuiLogTest extends SysuiTestCase {
private static final String TEST_ID = "TestLogger";
private static final int MAX_LOGS = 5;
@Mock
private DumpController mDumpController;
private SysuiLog mSysuiLog;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testLogDisabled_noLogsWritten() {
mSysuiLog = new SysuiLog(mDumpController, TEST_ID, MAX_LOGS, false);
assertEquals(mSysuiLog.mTimeline, null);
mSysuiLog.log(new Event("msg"));
assertEquals(mSysuiLog.mTimeline, null);
}
@Test
public void testLogEnabled_logWritten() {
mSysuiLog = new SysuiLog(mDumpController, TEST_ID, MAX_LOGS, true);
assertEquals(mSysuiLog.mTimeline.size(), 0);
mSysuiLog.log(new Event("msg"));
assertEquals(mSysuiLog.mTimeline.size(), 1);
}
@Test
public void testMaxLogs() {
mSysuiLog = new SysuiLog(mDumpController, TEST_ID, MAX_LOGS, true);
assertEquals(mSysuiLog.mTimeline.size(), 0);
final String msg = "msg";
for (int i = 0; i < MAX_LOGS + 1; i++) {
mSysuiLog.log(new Event(msg + i));
}
assertEquals(mSysuiLog.mTimeline.size(), MAX_LOGS);
// check the first message (msg0) is deleted:
assertEquals(mSysuiLog.mTimeline.getFirst().getMessage(), msg + "1");
}
}