Java tutorial
/** * Copyright 2015 Inrista * * 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.inrista.loggliest; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Process; import android.os.SystemClock; import android.util.Log; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import retrofit.RestAdapter; import retrofit.RetrofitError; import retrofit.client.Response; import retrofit.http.Body; import retrofit.http.Headers; import retrofit.http.POST; import retrofit.http.Path; import retrofit.mime.TypedByteArray; import retrofit.mime.TypedInput; /** * Android Loggly client using the HTTP/S bulk api. * <p> * First, initialize the global singleton instance using the * {@link Builder} provided by {@link #with(android.content.Context, String)}. * You must provide your Loggly token. A minimal setup looks like this: * <pre> * final String TOKEN = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" * Loggly.with(this, TOKEN).init(); * </pre> * Log a message: * <pre> * Loggly.i("stringfield", "test"); * Loggly.i("numberfield", 123); * </pre> * Log messages are formatted as json messages as expected by Loggly. Along with * the json field and value provided by calling one of the log functions, a "level" * field is appended with the current log level (verbose, debug, info, warning, error), * and a "timestamp" set according to when the message was logged. Also, a set of * various default fields can be appended, see {@link Builder#appendDefaultInfo(boolean)} * <p> * The log messages are saved in the internal app storage and uploaded to Loggly * in bulk when either one of these conditions are fulfilled: * <p> * <ul> * <li>1. The time since the last upload exceeds the * {@link Builder#uploadIntervalSecs(int seconds)} setting</li> * <li>2. The number of log messages since the last upload exceeds the * {@link Builder#uploadIntervalLogCount(int count)} setting</li> * <li>3. A new {@link Loggly} instance is created and there are (old) * messages that have not been uploaded</li> * <li>4. {@link #forceUpload()} is called</li> * </ul> * <p> * A typical configuration that sets upload intervals to 30mins, max number of messages * between uploads to 1000 messages, limits the internal app storage log buffer to 500k * and appends the default info fields along with a custom language field looks like this: * <pre> * Loggly.with(this, TOKEN) * .appendDefaultInfo(true) * .uploadIntervalLogCount(1000) * .uploadIntervalSecs(1800) * .maxSizeOnDisk(500000) * .appendStickyInfo("language", currentLanguage) * .init(); * </pre> * <b>NOTE:</b> Short upload intervals will have a negative effect on the battery life. * */ public class Loggly { private static final String LOGGLY_DEFAULT_URL = "https://logs-01.loggly.com"; private static final String LOG_FOLDER = "logglylogs"; private static final int LOGGLY_MAX_POST_SIZE = 1000000; private static final int MAX_SIZE_ON_DISK_DEFAULT = 10000000; private static final int MAX_SIZE_ON_DISK_MIN = 1000; private static final int UPLOAD_INTERVAL_SECS_MIN = 5; private static final int UPLOAD_INTERVAL_SECS_DEFAULT = 900; private static final int UPLOAD_INTERVAL_LOG_COUNT_MIN = 1; private static final int UPLOAD_INTERVAL_LOG_COUNT_DEFAULT = 500; private static final int IDLE_SECS_MIN = 0; private static final int IDLE_SECS_DEFAULT = 1200; private static final boolean APPEND_DEFAULT_INFO_DEFAULT = true; private static final String UPDATE_STICKY_INFO_MSG = "update-sticky-info"; private static final String THREAD_NAME = "loggliest"; private static volatile Loggly mInstance; private static volatile Builder mBuilder; private static Context mContext; private static String mToken; private static String mLogglyTag; private static int mUploadIntervalSecs; private static int mUploadIntervalLogCount; private static int mIdleSecs; private static boolean mAppendDefaultInfo; private static IBulkLog mBulkLogService; private static Thread mThread = null; private static LinkedBlockingQueue<JSONObject> mLogQueue = new LinkedBlockingQueue<JSONObject>(); private static long mLastUpload = 0; private static long mLastLog = 0; private static int mLogCounter = 0; private static File mRecentLogFile; private static SimpleDateFormat mDateFormat; private static String mAppVersionName = ""; private static int mAppVersionCode = 0; private static HashMap<String, String> mStickyInfo = null; private static int mMaxSizeOnDisk = 0; /** * Configures the {@link Loggly} instance. * */ public static class Builder { private final Context context; private final String token; private String logglyTag; private String logglyUrl; private int uploadIntervalSecs = UPLOAD_INTERVAL_SECS_DEFAULT; private int uploadIntervalLogCount = UPLOAD_INTERVAL_LOG_COUNT_DEFAULT; private int idleSecs = IDLE_SECS_DEFAULT; private boolean appendDefaultInfo = APPEND_DEFAULT_INFO_DEFAULT; private HashMap<String, String> stickyInfo = new HashMap<String, String>(); private int maxSizeOnDisk = MAX_SIZE_ON_DISK_DEFAULT; private Builder(Context context, String token) { this.token = token; this.context = context.getApplicationContext(); } /** * Set the Loggly tag that the log messages are tagged with. * If not specified this defaults to the package name. * @param logglyTag The Loggly tag */ public Builder tag(String logglyTag) { this.logglyTag = logglyTag; return this; } /** * Set the Loggly REST api endpoint. If not specified this defaults * to the HTTPS endpoint. * @param logglyUrl The api endpoint url */ public Builder url(String logglyUrl) { this.logglyUrl = logglyUrl; return this; } /** * Set the interval between Loggly log uploads. Note that short intervals * drains the battery. If not specified this defaults to 900 (15min). Also see * {@link #uploadIntervalLogCount(int count)}, when either condition is * fulfilled the logs will be uploaded. * @param seconds Time in seconds */ public Builder uploadIntervalSecs(int seconds) { this.uploadIntervalSecs = seconds; return this; } /** * Set the interval between Loggly log uploads. Note that small intervals * drains the battery if you log many messages. If not specified this * defaults to 500. Also see {@link #uploadIntervalSecs(int seconds)}, * when either condition is fulfilled the logs will be uploaded. * @param count Number of logs */ public Builder uploadIntervalLogCount(int count) { this.uploadIntervalLogCount = count; return this; } /** * Set the time before the background log processing stops if no messages have * been logged. This processing is automatically resumed when a new message is logged. * This defaults to 1200 seconds (20min) if not specified. * @param seconds Time in seconds * @return */ public Builder idleSecs(int seconds) { this.idleSecs = seconds; return this; } /** * Set whether four default key-value pairs should be appended to each logged message: * <ul> * <li>"appversionname" - verisonName from the manifest</li> * <li>"appversioncode" - versionCode from the manifest</li> * <li>"devicemodel" - the android.os.Build.MODEL constant</li> * <li>"androidversioncode" - the android.os.Build.VERSION.SDK_INT constant</li> * </ul> * @param append Set to true to append default info */ public Builder appendDefaultInfo(boolean append) { this.appendDefaultInfo = append; return this; } /** * Append a key-value pair to each logged message. Chain multiple appendStickyInfo * to append multiple key-value pairs. * @param key The Loggly json field * @param value The value */ public Builder appendStickyInfo(String key, String value) { this.stickyInfo.put(key, value); return this; } /** * Configure the buffer size for log messages that are not yet uploaded. * The oldest messages will be dropped if more info is logged than what * fits in the buffer. * @param size Max size in bytes * @return */ public Builder maxSizeOnDisk(int size) { this.maxSizeOnDisk = size; return this; } /** * Create the {@link Loggly} instance. */ public Loggly init() { if (logglyTag == null) logglyTag = context.getPackageName(); if (logglyUrl == null) logglyUrl = LOGGLY_DEFAULT_URL; if (uploadIntervalSecs < UPLOAD_INTERVAL_SECS_MIN) uploadIntervalSecs = UPLOAD_INTERVAL_SECS_MIN; if (uploadIntervalLogCount < UPLOAD_INTERVAL_LOG_COUNT_MIN) uploadIntervalLogCount = UPLOAD_INTERVAL_LOG_COUNT_MIN; if (idleSecs < IDLE_SECS_MIN) idleSecs = IDLE_SECS_MIN; if (maxSizeOnDisk < MAX_SIZE_ON_DISK_MIN) maxSizeOnDisk = MAX_SIZE_ON_DISK_MIN; if (mInstance == null) { synchronized (Loggly.class) { if (mInstance == null) { mInstance = new Loggly(context, token, logglyTag, logglyUrl, uploadIntervalSecs, uploadIntervalLogCount, idleSecs, appendDefaultInfo, stickyInfo, maxSizeOnDisk); } } } return mInstance; } } /** * Creates a {@link Builder} instance. * @param context Context * @param token Loggly token * @return A {@link Builder} instance */ public static Builder with(Context context, String token) { if (mBuilder == null) { synchronized (Loggly.class) { if (mBuilder == null) { mBuilder = new Builder(context, token); } } } return mBuilder; } private Loggly(Context context, String token, String logglyTag, String logglyUrl, int uploadIntervalSecs, int uploadIntervalLogCount, int idleSecs, boolean appendDefaultInfo, HashMap<String, String> stickyInfo, int maxSizeOnDisk) { mContext = context; mToken = token; mLogglyTag = logglyTag; mUploadIntervalSecs = uploadIntervalSecs; mUploadIntervalLogCount = uploadIntervalLogCount; mIdleSecs = idleSecs; mAppendDefaultInfo = appendDefaultInfo; mStickyInfo = stickyInfo; mMaxSizeOnDisk = maxSizeOnDisk; mRecentLogFile = recentLogFile(); mDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US); PackageManager manager = context.getPackageManager(); try { PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0); mAppVersionName = info.versionName; mAppVersionCode = info.versionCode; } catch (NameNotFoundException e) { } RestAdapter restAdapter = new RestAdapter.Builder().setEndpoint(logglyUrl).build(); mBulkLogService = restAdapter.create(IBulkLog.class); start(); } private static synchronized void start() { if (mThread != null && mThread.isAlive()) return; mThread = new Thread(new Runnable() { @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); mThread.setName(THREAD_NAME); List<JSONObject> logBatch = new ArrayList<JSONObject>(); while (true) { try { JSONObject msg = mLogQueue.poll(10, TimeUnit.SECONDS); if (msg != null) { logBatch.add(msg); while ((msg = mLogQueue.poll()) != null) logBatch.add(msg); } long now = SystemClock.elapsedRealtime() / 1000; if (!logBatch.isEmpty()) { mLastLog = now; mLogCounter += logBatch.size(); logToFile(logBatch); logBatch.clear(); } if ((now - mLastUpload) >= mUploadIntervalSecs || mLogCounter >= mUploadIntervalLogCount) { postLogs(); } if (((now - mLastLog) >= mIdleSecs) && mIdleSecs > 0 && mLastLog > 0) { mThread.interrupt(); } } catch (InterruptedException e) { mLogQueue.drainTo(logBatch); logToFile(logBatch); postLogs(); return; } } } }); mThread.start(); } private static void log(JSONObject jsonObject) { if (!mThread.isAlive()) start(); mLogQueue.offer(jsonObject); } private static void log(String key, Object msg, String level, long time) { JSONObject json = new JSONObject(); try { json.put("timestamp", time); json.put("level", level); json.put(key, msg); log(json); } catch (JSONException e) { } } /** * Log a verbose message. * @param key Loggly json field * @param msg The log message, either a JSONObject, String, Boolean, Integer, Long or Double */ public static void v(String key, Object msg) { log(key, msg, "verbose", System.currentTimeMillis()); } /** * Log a verbose message and an exception. * @param key Loggly json field * @param msg The log message * @param tr The exception to log */ public static void v(String key, String msg, Throwable tr) { v(key, msg + '\n' + Log.getStackTraceString(tr)); } /** * Log a debug message. * @param key Loggly json field * @param msg The log message, either a JSONObject, String, Boolean, Integer, Long or Double */ public static void d(String key, Object msg) { log(key, msg, "debug", System.currentTimeMillis()); } /** * Log a debug message and an exception. * @param key Loggly json field * @param msg The log message * @param tr The exception to log */ public static void d(String key, String msg, Throwable tr) { d(key, msg + '\n' + Log.getStackTraceString(tr)); } /** * Log an info message. * @param key Loggly json field * @param msg The log message, either a JSONObject, String, Boolean, Integer, Long or Double */ public static void i(String key, Object msg) { log(key, msg, "info", System.currentTimeMillis()); } /** * Log an info message and an exception. * @param key Loggly json field * @param msg The log message * @param tr The exception to log */ public static void i(String key, String msg, Throwable tr) { i(key, msg + '\n' + Log.getStackTraceString(tr)); } /** * Log a warning message. * @param key Loggly json field * @param msg The log message, either a JSONObject, String, Boolean, Integer, Long or Double */ public static void w(String key, Object msg) { log(key, msg, "warning", System.currentTimeMillis()); } /** * Log a warning message and an exception. * @param key Loggly json field * @param msg The log message * @param tr The exception to log */ public static void w(String key, String msg, Throwable tr) { w(key, msg + '\n' + Log.getStackTraceString(tr)); } /** * Log an error message. * @param key Loggly json field * @param msg The log message, either a JSONObject, String, Boolean, Integer, Long or Double */ public static void e(String key, Object msg) { log(key, msg, "error", System.currentTimeMillis()); } /** * Log an error message and an exception. * @param key Loggly json field * @param msg The log message * @param tr The exception to log */ public static void e(String key, String msg, Throwable tr) { e(key, msg + '\n' + Log.getStackTraceString(tr)); } /** * Force an upload of all log messages that have not yet been uploaded. * This will block the current thread, don't call from the UI thread. * <p> * Note that all log messages will be uploaded every time a new Loggly * instance is created, so this is just to provide a point of * synchronization if necessary. */ public static synchronized void forceUpload() { mThread.interrupt(); try { mThread.join(); } catch (InterruptedException e) { } } /** * Set or reset a key-value pair that is appended to each Loggly json log message. * @param key The Loggly json field * @param msg The message */ public static void setStickyInfo(String key, String msg) { JSONObject json = new JSONObject(); try { json.put(UPDATE_STICKY_INFO_MSG, true); json.put("key", key); json.put("msg", msg); log(json); } catch (JSONException e) { } } private static File recentLogFile() { File dir = mContext.getDir(LOG_FOLDER, Context.MODE_PRIVATE); File[] files = dir.listFiles(); if (files == null || files.length == 0) { return null; } File file = files[0]; for (int i = 1; i < files.length; i++) { if (file.lastModified() < files[i].lastModified()) { file = files[i]; } } return file; } private static File oldestLogFile() { File dir = mContext.getDir(LOG_FOLDER, Context.MODE_PRIVATE); File[] files = dir.listFiles(); if (files == null || files.length == 0) { return null; } File file = files[0]; for (int i = 1; i < files.length; i++) { if (file.lastModified() > files[i].lastModified()) { file = files[i]; } } return file; } private static File createLogFile() { File dir = mContext.getDir(LOG_FOLDER, Context.MODE_PRIVATE); File logFile = new File(dir, Long.toString(System.currentTimeMillis())); return logFile; } private static void logToFile(List<JSONObject> msgBatch) { if (msgBatch.isEmpty()) return; try { // Size of logs on disk File dir = mContext.getDir(LOG_FOLDER, Context.MODE_PRIVATE); long totalSize = 0; File[] logFiles = dir.listFiles(); for (File logFile : logFiles) totalSize += logFile.length(); // Check if size of logs on disk exceeds the limit, drop // oldest messages in this case if (totalSize > mMaxSizeOnDisk) { int numFiles = logFiles.length; if (numFiles <= 1) return; oldestLogFile().delete(); } // Create a new log file if necessary if (mRecentLogFile == null || mRecentLogFile.length() > LOGGLY_MAX_POST_SIZE) mRecentLogFile = createLogFile(); PrintStream ps = new PrintStream(new FileOutputStream(mRecentLogFile, true)); for (JSONObject msg : msgBatch) { try { if (msg.has(UPDATE_STICKY_INFO_MSG)) { mStickyInfo.put(msg.getString("key"), msg.getString("msg")); continue; } // Reformat timestamp to ISO-8601 as expected by Loggly long timestamp = msg.getLong("timestamp"); msg.remove("timestamp"); msg.put("timestamp", mDateFormat.format(timestamp)); // Append default info if (mAppendDefaultInfo) { msg.put("appversionname", mAppVersionName); msg.put("appversioncode", Integer.toString(mAppVersionCode)); msg.put("devicemodel", android.os.Build.MODEL); msg.put("androidversioncode", Integer.toString(android.os.Build.VERSION.SDK_INT)); } // Append sticky info if (!mStickyInfo.isEmpty()) { for (String key : mStickyInfo.keySet()) msg.put(key, mStickyInfo.get(key)); } } catch (JSONException e) { } ps.println(msg.toString().replace("\n", "\\n")); } ps.close(); } catch (FileNotFoundException e) { } } private interface IBulkLog { @Headers("content-type:application/json") @POST("/bulk/{token}/tag/{tag}") Response log(@Path("token") String token, @Path("tag") String logtag, @Body TypedInput body); } private static void postLogs() { mLastUpload = SystemClock.elapsedRealtime() / 1000; mLogCounter = 0; File dir = mContext.getDir(LOG_FOLDER, Context.MODE_PRIVATE); for (File logFile : dir.listFiles()) { StringBuilder builder = new StringBuilder(); try { // Read log files and build body of newline-separated json log entries FileInputStream fis = new FileInputStream(logFile); BufferedReader reader = new BufferedReader(new InputStreamReader(fis)); String line; while ((line = reader.readLine()) != null) { builder.append(line); builder.append('\n'); } reader.close(); } catch (FileNotFoundException e) { return; } catch (IOException e) { return; } String json = builder.toString(); if (json.isEmpty()) return; try { // Blocking POST TypedInput body = new TypedByteArray("application/json", json.getBytes()); Response answer = mBulkLogService.log(mToken, mLogglyTag, body); // Successful post if (answer.getStatus() == 200) { logFile.delete(); mRecentLogFile = null; } } // Post failed for some reason, keep log files and retry later catch (RetrofitError error) { } } } }