Log sync details to rotating log files (userdebug/eng only)
Bug 62052247
Test: Manual test with setting debug.synclog to 0 and 1.
Change-Id: I553dc8d3457ae99cbca5bf6a74303b8a8d8817e7
diff --git a/services/core/java/com/android/server/content/SyncLogger.java b/services/core/java/com/android/server/content/SyncLogger.java
new file mode 100644
index 0000000..db79464
--- /dev/null
+++ b/services/core/java/com/android/server/content/SyncLogger.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2017 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.server.content;
+
+import android.app.job.JobParameters;
+import android.os.Build;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.SystemProperties;
+import android.text.format.DateUtils;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import libcore.io.IoUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Implements a rotating file logger for the sync manager, which is enabled only on userdebug/eng
+ * builds (unless debug.synclog is set to 1).
+ *
+ * Note this class could be used for other purposes too, but in general we don't want various
+ * system components to log to files, so it's put in a local package here.
+ */
+public class SyncLogger {
+ private static final String TAG = "SyncLogger";
+
+ private static SyncLogger sInstance;
+
+ SyncLogger() {
+ }
+
+ /**
+ * @return the singleton instance.
+ */
+ public static synchronized SyncLogger getInstance() {
+ if (sInstance == null) {
+ final boolean enable = "1".equals(SystemProperties.get("debug.synclog",
+ Build.IS_DEBUGGABLE ? "1" : "0"));
+ if (enable) {
+ sInstance = new RotatingFileLogger();
+ } else {
+ sInstance = new SyncLogger();
+ }
+ }
+ return sInstance;
+ }
+
+ /**
+ * Write strings to the log file.
+ */
+ public void log(Object... message) {
+ }
+
+ /**
+ * Remove old log files.
+ */
+ public void purgeOldLogs() {
+ // The default implementation is no-op.
+ }
+
+ public String jobParametersToString(JobParameters params) {
+ // The default implementation is no-op.
+ return "";
+ }
+
+ /**
+ * Dump all existing log files into a given writer.
+ */
+ public void dumpAll(PrintWriter pw) {
+ }
+
+ /**
+ * Actual implementation which is only used on userdebug/eng builds (by default).
+ */
+ private static class RotatingFileLogger extends SyncLogger {
+ private final Object mLock = new Object();
+
+ private final long mKeepAgeMs = TimeUnit.DAYS.toMillis(7);
+
+ private static final SimpleDateFormat sTimestampFormat
+ = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+
+ private static final SimpleDateFormat sFilenameDateFormat
+ = new SimpleDateFormat("yyyy-MM-dd");
+
+ @GuardedBy("mLock")
+ private final Date mCachedDate = new Date();
+
+ @GuardedBy("mLock")
+ private final StringBuilder mStringBuilder = new StringBuilder();
+
+ private final File mLogPath;
+
+ @GuardedBy("mLock")
+ private long mCurrentLogFileDayTimestamp;
+
+ @GuardedBy("mLock")
+ private Writer mLogWriter;
+
+ @GuardedBy("mLock")
+ private boolean mErrorShown;
+
+ RotatingFileLogger() {
+ mLogPath = new File(Environment.getDataSystemDirectory(), "syncmanager-log");
+ }
+
+ private void handleException(String message, Exception e) {
+ if (!mErrorShown) {
+ Slog.e(TAG, message, e);
+ mErrorShown = true;
+ }
+ }
+
+ @Override
+ public void log(Object... message) {
+ if (message == null) {
+ return;
+ }
+ synchronized (mLock) {
+ final long now = System.currentTimeMillis();
+ openLogLocked(now);
+ if (mLogWriter == null) {
+ return; // Couldn't open log file?
+ }
+
+ mStringBuilder.setLength(0);
+ mCachedDate.setTime(now);
+ mStringBuilder.append(sTimestampFormat.format(mCachedDate));
+ mStringBuilder.append(' ');
+
+ mStringBuilder.append(android.os.Process.myTid());
+ mStringBuilder.append(' ');
+
+ for (Object o : message) {
+ mStringBuilder.append(o);
+ }
+ mStringBuilder.append('\n');
+
+ try {
+ mLogWriter.append(mStringBuilder);
+ mLogWriter.flush();
+ } catch (IOException e) {
+ handleException("Failed to write log", e);
+ }
+ }
+ }
+
+ private void openLogLocked(long now) {
+ // If we already have a log file opened and the date has't changed, just use it.
+ final long day = now % DateUtils.DAY_IN_MILLIS;
+ if ((mLogWriter != null) && (day == mCurrentLogFileDayTimestamp)) {
+ return;
+ }
+
+ // Otherwise create a new log file.
+ closeCurrentLogLocked();
+
+ mCurrentLogFileDayTimestamp = day;
+
+ mCachedDate.setTime(now);
+ final String filename = "synclog-" + sFilenameDateFormat.format(mCachedDate) + ".log";
+ final File file = new File(mLogPath, filename);
+
+ file.getParentFile().mkdirs();
+
+ try {
+ mLogWriter = new FileWriter(file, /* append= */ true);
+ } catch (IOException e) {
+ handleException("Failed to open log file: " + file, e);
+ }
+ }
+
+ private void closeCurrentLogLocked() {
+ IoUtils.closeQuietly(mLogWriter);
+ mLogWriter = null;
+ }
+
+ @Override
+ public void purgeOldLogs() {
+ synchronized (mLock) {
+ FileUtils.deleteOlderFiles(mLogPath, /* keepCount= */ 1, mKeepAgeMs);
+ }
+ }
+
+ @Override
+ public String jobParametersToString(JobParameters params) {
+ if (params == null) {
+ return "job:null";
+ } else {
+ return "job:#" + params.getJobId() + ":"
+ + SyncOperation.maybeCreateFromJobExtras(params.getExtras());
+ }
+ }
+
+ @Override
+ public void dumpAll(PrintWriter pw) {
+ synchronized (mLock) {
+ final String[] files = mLogPath.list();
+ if (files == null || (files.length == 0)) {
+ return;
+ }
+ Arrays.sort(files);
+
+ for (String file : files) {
+ dumpFile(pw, new File(mLogPath, file));
+ }
+ }
+ }
+
+ private void dumpFile(PrintWriter pw, File file) {
+ Slog.w(TAG, "Dumping " + file);
+ final char[] buffer = new char[32 * 1024];
+
+ try (Reader in = new BufferedReader(new FileReader(file))) {
+ int read;
+ while ((read = in.read(buffer)) >= 0) {
+ if (read > 0) {
+ pw.write(buffer, 0, read);
+ }
+ }
+ } catch (IOException e) {
+ }
+ }
+ }
+}