1 /* 2 * Copyright (C) 2015 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.messaging.util; 18 19 import android.os.Process; 20 import android.util.Log; 21 22 import com.android.messaging.Factory; 23 24 import java.io.BufferedReader; 25 import java.io.File; 26 import java.io.FileNotFoundException; 27 import java.io.FileReader; 28 import java.io.IOException; 29 import java.io.PrintWriter; 30 import java.text.SimpleDateFormat; 31 import java.util.logging.FileHandler; 32 import java.util.logging.Formatter; 33 import java.util.logging.Handler; 34 import java.util.logging.Level; 35 import java.util.logging.Logger; 36 37 /** 38 * Save the app's own log to dump along with adb bugreport 39 */ 40 public abstract class LogSaver { 41 /** 42 * Writes the accumulated log entries, from oldest to newest, to the specified PrintWriter. 43 * Log lines are emitted in much the same form as logcat -v threadtime -- specifically, 44 * lines will include a timestamp, pid, tid, level, and tag. 45 * 46 * @param writer The PrintWriter to output 47 */ dump(PrintWriter writer)48 public abstract void dump(PrintWriter writer); 49 50 /** 51 * Log a line 52 * 53 * @param level The log level to use 54 * @param tag The log tag 55 * @param msg The message of the log line 56 */ log(int level, String tag, String msg)57 public abstract void log(int level, String tag, String msg); 58 59 /** 60 * Check if the LogSaver still matches the current Gservices settings 61 * 62 * @return true if matches, false otherwise 63 */ isCurrent()64 public abstract boolean isCurrent(); 65 LogSaver()66 private LogSaver() { 67 } 68 newInstance()69 public static LogSaver newInstance() { 70 final boolean persistent = BugleGservices.get().getBoolean( 71 BugleGservicesKeys.PERSISTENT_LOGSAVER, 72 BugleGservicesKeys.PERSISTENT_LOGSAVER_DEFAULT); 73 if (persistent) { 74 final int setSize = BugleGservices.get().getInt( 75 BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE, 76 BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE_DEFAULT); 77 final int fileLimitBytes = BugleGservices.get().getInt( 78 BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES, 79 BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES_DEFAULT); 80 return new DiskLogSaver(setSize, fileLimitBytes); 81 } else { 82 final int size = BugleGservices.get().getInt( 83 BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT, 84 BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT_DEFAULT); 85 return new MemoryLogSaver(size); 86 } 87 } 88 89 /** 90 * A circular in-memory log to be used to log potentially verbose logs. The logs will be 91 * persisted in memory in the application and can be dumped by various dump() methods. 92 * For example, adb shell dumpsys activity provider com.android.messaging. 93 * The dump will also show up in bugreports. 94 */ 95 private static final class MemoryLogSaver extends LogSaver { 96 /** 97 * Record to store a single log entry. Stores timestamp, tid, level, tag, and message. 98 * It can be reused when the circular log rolls over. This avoids creating new objects. 99 */ 100 private static class LogRecord { 101 int mTid; 102 String mLevelString; 103 long mTimeMillis; // from System.currentTimeMillis 104 String mTag; 105 String mMessage; 106 LogRecord()107 LogRecord() { 108 } 109 set(int tid, int level, long time, String tag, String message)110 void set(int tid, int level, long time, String tag, String message) { 111 this.mTid = tid; 112 this.mTimeMillis = time; 113 this.mTag = tag; 114 this.mMessage = message; 115 this.mLevelString = getLevelString(level); 116 } 117 } 118 119 private final int mSize; 120 private final CircularArray<LogRecord> mLogList; 121 private final Object mLock; 122 123 private final SimpleDateFormat mSdf = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); 124 MemoryLogSaver(final int size)125 public MemoryLogSaver(final int size) { 126 mSize = size; 127 mLogList = new CircularArray<LogRecord>(size); 128 mLock = new Object(); 129 } 130 131 @Override dump(PrintWriter writer)132 public void dump(PrintWriter writer) { 133 int pid = Process.myPid(); 134 synchronized (mLock) { 135 for (int i = 0; i < mLogList.count(); i++) { 136 LogRecord rec = mLogList.get(i); 137 writer.println(String.format("%s %5d %5d %s %s: %s", 138 mSdf.format(rec.mTimeMillis), 139 pid, rec.mTid, rec.mLevelString, rec.mTag, rec.mMessage)); 140 } 141 } 142 } 143 144 @Override log(int level, String tag, String msg)145 public void log(int level, String tag, String msg) { 146 synchronized (mLock) { 147 LogRecord rec = mLogList.getFree(); 148 if (rec == null) { 149 rec = new LogRecord(); 150 } 151 rec.set(Process.myTid(), level, System.currentTimeMillis(), tag, msg); 152 mLogList.add(rec); 153 } 154 } 155 156 @Override isCurrent()157 public boolean isCurrent() { 158 final boolean persistent = BugleGservices.get().getBoolean( 159 BugleGservicesKeys.PERSISTENT_LOGSAVER, 160 BugleGservicesKeys.PERSISTENT_LOGSAVER_DEFAULT); 161 if (persistent) { 162 return false; 163 } 164 final int size = BugleGservices.get().getInt( 165 BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT, 166 BugleGservicesKeys.IN_MEMORY_LOGSAVER_RECORD_COUNT_DEFAULT); 167 return size == mSize; 168 } 169 } 170 171 /** 172 * A persistent, on-disk log saver. It uses the standard Java util logger along with 173 * a rotation log file set to store the logs in app's local file directory "app_logs". 174 */ 175 private static final class DiskLogSaver extends LogSaver { 176 private static final String DISK_LOG_DIR_NAME = "logs"; 177 178 private final int mSetSize; 179 private final int mFileLimitBytes; 180 private Logger mDiskLogger; 181 DiskLogSaver(final int setSize, final int fileLimitBytes)182 public DiskLogSaver(final int setSize, final int fileLimitBytes) { 183 Assert.isTrue(setSize > 0); 184 Assert.isTrue(fileLimitBytes > 0); 185 mSetSize = setSize; 186 mFileLimitBytes = fileLimitBytes; 187 initDiskLog(); 188 } 189 clearDefaultHandlers(Logger logger)190 private static void clearDefaultHandlers(Logger logger) { 191 Assert.notNull(logger); 192 for (Handler handler : logger.getHandlers()) { 193 logger.removeHandler(handler); 194 } 195 } 196 initDiskLog()197 private void initDiskLog() { 198 mDiskLogger = Logger.getLogger(LogUtil.BUGLE_TAG); 199 // We don't want the default console handler 200 clearDefaultHandlers(mDiskLogger); 201 // Don't want duplicate print in system log 202 mDiskLogger.setUseParentHandlers(false); 203 // FileHandler manages the log files in a fixed rotation set 204 final File logDir = Factory.get().getApplicationContext().getDir( 205 DISK_LOG_DIR_NAME, 0/*mode*/); 206 FileHandler handler = null; 207 try { 208 handler = new FileHandler( 209 logDir + "/%g.log", mFileLimitBytes, mSetSize, true/*append*/); 210 } catch (Exception e) { 211 Log.e(LogUtil.BUGLE_TAG, "LogSaver: fail to init disk logger", e); 212 return; 213 } 214 final Formatter formatter = new Formatter() { 215 @Override 216 public String format(java.util.logging.LogRecord r) { 217 return r.getMessage(); 218 } 219 }; 220 handler.setFormatter(formatter); 221 handler.setLevel(Level.ALL); 222 mDiskLogger.addHandler(handler); 223 } 224 225 @Override dump(PrintWriter writer)226 public void dump(PrintWriter writer) { 227 for (int i = mSetSize - 1; i >= 0; i--) { 228 final File logDir = Factory.get().getApplicationContext().getDir( 229 DISK_LOG_DIR_NAME, 0/*mode*/); 230 final String logFilePath = logDir + "/" + i + ".log"; 231 try { 232 final File logFile = new File(logFilePath); 233 if (!logFile.exists()) { 234 continue; 235 } 236 final BufferedReader reader = new BufferedReader(new FileReader(logFile)); 237 for (String line; (line = reader.readLine()) != null;) { 238 line = line.trim(); 239 writer.println(line); 240 } 241 } catch (FileNotFoundException e) { 242 Log.w(LogUtil.BUGLE_TAG, "LogSaver: can not find log file " + logFilePath); 243 } catch (IOException e) { 244 Log.w(LogUtil.BUGLE_TAG, "LogSaver: can not read log file", e); 245 } 246 } 247 } 248 249 @Override log(int level, String tag, String msg)250 public void log(int level, String tag, String msg) { 251 final SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); 252 mDiskLogger.info(String.format("%s %5d %5d %s %s: %s\n", 253 sdf.format(System.currentTimeMillis()), 254 Process.myPid(), Process.myTid(), getLevelString(level), tag, msg)); 255 } 256 257 @Override isCurrent()258 public boolean isCurrent() { 259 final boolean persistent = BugleGservices.get().getBoolean( 260 BugleGservicesKeys.PERSISTENT_LOGSAVER, 261 BugleGservicesKeys.PERSISTENT_LOGSAVER_DEFAULT); 262 if (!persistent) { 263 return false; 264 } 265 final int setSize = BugleGservices.get().getInt( 266 BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE, 267 BugleGservicesKeys.PERSISTENT_LOGSAVER_ROTATION_SET_SIZE_DEFAULT); 268 final int fileLimitBytes = BugleGservices.get().getInt( 269 BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES, 270 BugleGservicesKeys.PERSISTENT_LOGSAVER_FILE_LIMIT_BYTES_DEFAULT); 271 return setSize == mSetSize && fileLimitBytes == mFileLimitBytes; 272 } 273 } 274 getLevelString(final int level)275 private static String getLevelString(final int level) { 276 switch (level) { 277 case android.util.Log.DEBUG: 278 return "D"; 279 case android.util.Log.WARN: 280 return "W"; 281 case android.util.Log.INFO: 282 return "I"; 283 case android.util.Log.VERBOSE: 284 return "V"; 285 case android.util.Log.ERROR: 286 return "E"; 287 case android.util.Log.ASSERT: 288 return "A"; 289 default: 290 return "?"; 291 } 292 } 293 } 294