1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.utils;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.util.Log;
23 
24 import java.io.PrintWriter;
25 import java.lang.annotation.Retention;
26 import java.lang.annotation.RetentionPolicy;
27 import java.text.SimpleDateFormat;
28 import java.util.ArrayDeque;
29 import java.util.ArrayList;
30 import java.util.Date;
31 import java.util.List;
32 import java.util.Locale;
33 
34 /**
35  * Logs human-readable events for debugging purposes.
36  */
37 public class EventLogger {
38 
39     /** Prefix for the title added at the beginning of a {@link #dump(PrintWriter)} operation */
40     private static final String DUMP_TITLE_PREFIX = "Events log: ";
41 
42     /** Identifies the source of events. */
43     @Nullable private final String mTag;
44 
45     /** Stores the events using a ring buffer. */
46     private final ArrayDeque<Event> mEvents;
47 
48     /**
49      * The maximum number of events to keep in {@code mEvents}.
50      *
51      * <p>Calling {@link #enqueue} when the size of {@link #mEvents} matches the threshold will
52      * cause the oldest event to be evicted.
53      */
54     private final int mMemSize;
55 
56     /**
57      * Constructor for logger.
58      * @param size the maximum number of events to keep in log
59      * @param tag the string displayed before the recorded log
60      */
EventLogger(int size, @Nullable String tag)61     public EventLogger(int size, @Nullable String tag) {
62         mEvents = new ArrayDeque<>(size);
63         mMemSize = size;
64         mTag = tag;
65     }
66 
67     /** Enqueues {@code event} to be logged. */
enqueue(Event event)68     public synchronized void enqueue(Event event) {
69         if (mEvents.size() >= mMemSize) {
70             mEvents.removeFirst();
71         }
72 
73         mEvents.addLast(event);
74     }
75 
76     /**
77      * Add a string-based event to the log, and print it to logcat with a specific severity.
78      * @param msg the message for the logs
79      * @param logType the type of logcat entry
80      * @param tag the logcat tag to use
81      */
enqueueAndLog(String msg, @Event.LogType int logType, String tag)82     public synchronized void enqueueAndLog(String msg, @Event.LogType int logType, String tag) {
83         final Event event = new StringEvent(msg);
84         enqueue(event.printLog(logType, tag));
85     }
86 
87     /** Dumps events into the given {@link DumpSink}. */
dump(DumpSink dumpSink)88     public synchronized void dump(DumpSink dumpSink) {
89         dumpSink.sink(mTag, new ArrayList<>(mEvents));
90     }
91 
92     /** Dumps events using {@link PrintWriter}. */
dump(PrintWriter pw)93     public synchronized void dump(PrintWriter pw) {
94         dump(pw, "" /* prefix */);
95     }
96 
getDumpTitle()97     protected String getDumpTitle() {
98         if (mTag == null) {
99             return DUMP_TITLE_PREFIX;
100         }
101         return DUMP_TITLE_PREFIX + mTag;
102     }
103 
104     /** Dumps events using {@link PrintWriter} with a certain indent. */
dump(PrintWriter pw, String indent)105     public synchronized void dump(PrintWriter pw, String indent) {
106         pw.println(getDumpTitle());
107 
108         for (Event evt : mEvents) {
109             pw.println(indent + evt.toString());
110         }
111     }
112 
113     /** Receives events from {@link EventLogger} upon a {@link #dump(DumpSink)} call. **/
114     public interface DumpSink {
115 
116         /** Processes given events into some pipeline with a given tag. **/
sink(String tag, List<Event> events)117         void sink(String tag, List<Event> events);
118 
119     }
120 
121     public abstract static class Event {
122 
123         /** Timestamps formatter. */
124         private static final SimpleDateFormat sFormat =
125                 new SimpleDateFormat("MM-dd HH:mm:ss:SSS", Locale.US);
126 
127         private final long mTimestamp;
128 
Event()129         public Event() {
130             mTimestamp = System.currentTimeMillis();
131         }
132 
toString()133         public String toString() {
134             return (new StringBuilder(sFormat.format(new Date(mTimestamp))))
135                     .append(" ").append(eventToString()).toString();
136         }
137 
138         /**
139          * Causes the string message for the event to appear in the logcat.
140          * Here is an example of how to create a new event (a StringEvent), adding it to the logger
141          * (an instance of AudioEventLogger) while also making it show in the logcat:
142          * <pre>
143          *     myLogger.log(
144          *         (new StringEvent("something for logcat and logger")).printLog(MyClass.TAG) );
145          * </pre>
146          * @param tag the tag for the android.util.Log.v
147          * @return the same instance of the event
148          */
printLog(String tag)149         public Event printLog(String tag) {
150             return printLog(ALOGI, tag);
151         }
152 
153         /** @hide */
154         @IntDef(flag = false, value = {
155                 ALOGI,
156                 ALOGE,
157                 ALOGW,
158                 ALOGV }
159         )
160         @Retention(RetentionPolicy.SOURCE)
161         public @interface LogType {}
162 
163         public static final int ALOGI = 0;
164         public static final int ALOGE = 1;
165         public static final int ALOGW = 2;
166         public static final int ALOGV = 3;
167 
168         /**
169          * Same as {@link #printLog(String)} with a log type
170          * @param type one of {@link #ALOGI}, {@link #ALOGE}, {@link #ALOGV}
171          * @param tag
172          * @return
173          */
printLog(@ogType int type, String tag)174         public Event printLog(@LogType int type, String tag) {
175             switch (type) {
176                 case ALOGI:
177                     Log.i(tag, eventToString());
178                     break;
179                 case ALOGE:
180                     Log.e(tag, eventToString());
181                     break;
182                 case ALOGW:
183                     Log.w(tag, eventToString());
184                     break;
185                 case ALOGV:
186                 default:
187                     Log.v(tag, eventToString());
188                     break;
189             }
190             return this;
191         }
192 
193         /**
194          * Convert event to String.
195          * This method is only called when the logger history is about to the dumped,
196          * so this method is where expensive String conversions should be made, not when the Event
197          * subclass is created.
198          * Timestamp information will be automatically added, do not include it.
199          * @return a string representation of the event that occurred.
200          */
eventToString()201         public abstract String eventToString();
202     }
203 
204     public static class StringEvent extends Event {
205 
206         @Nullable
207         private final String mSource;
208 
209         private final String mDescription;
210 
211         /** Creates event from {@code source} and formatted {@code description} with {@code args} */
from(@onNull String source, @NonNull String description, Object... args)212         public static StringEvent from(@NonNull String source,
213                 @NonNull String description, Object... args) {
214             return new StringEvent(source, String.format(Locale.US, description, args));
215         }
216 
StringEvent(String description)217         public StringEvent(String description) {
218             this(null /* source */, description);
219         }
220 
StringEvent(String source, String description)221         public StringEvent(String source, String description) {
222             mSource = source;
223             mDescription = description;
224         }
225 
226         @Override
eventToString()227         public String eventToString() {
228             if (mSource == null) {
229                 return mDescription;
230             }
231 
232             // [source ] optional description
233             return String.format("[%-40s] %s",
234                     mSource,
235                     (mDescription == null ? "" : mDescription));
236         }
237     }
238 }
239