Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d6aec9d2 authored by Beverly Tai's avatar Beverly Tai Committed by Android (Google) Code Review
Browse files

Merge "New SysUi logger"

parents dcad11ff c65c4087
Loading
Loading
Loading
Loading
+67 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+107 −0
Original line number Diff line number Diff line
/*
 * 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;
        }
    }
}
+163 −0
Original line number Diff line number Diff line
/*
 * 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;
}
+69 −0
Original line number Diff line number Diff line
/*
 * 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"};
        }
    }

}
+82 −0
Original line number Diff line number Diff line
/*
 * 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");
    }
}