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

Commit c65c4087 authored by Beverly's avatar Beverly
Browse files

New SysUi logger

Test: atest SysuiLogTest
Test: atest RichEventTest
Bug: 141470043
Change-Id: Ied8ccc58734490230039c851cd3f020a048642d8
parent 7ef9e2f9
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");
    }
}