├── .gitignore ├── .travis.yml ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── logentries │ ├── logger │ ├── AndroidLogger.java │ ├── AsyncLoggingWorker.java │ └── LogStorage.java │ ├── misc │ └── Utils.java │ └── net │ └── LogentriesClient.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/jetbrains,java,gradle,android,windows,osx 3 | 4 | ### JetBrains ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | .idea 8 | # User-specific stuff: 9 | .idea/workspace.xml 10 | .idea/tasks.xml 11 | .idea/dictionaries 12 | .idea/vcs.xml 13 | .idea/jsLibraryMappings.xml 14 | 15 | # Sensitive or high-churn files: 16 | .idea/dataSources.ids 17 | .idea/dataSources.xml 18 | .idea/dataSources.local.xml 19 | .idea/sqlDataSources.xml 20 | .idea/dynamic.xml 21 | .idea/uiDesigner.xml 22 | 23 | # Gradle: 24 | .idea/gradle.xml 25 | .idea/libraries 26 | 27 | # Mongo Explorer plugin: 28 | .idea/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | /out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Crashlytics plugin (for Android Studio and IntelliJ) 45 | com_crashlytics_export_strings.xml 46 | crashlytics.properties 47 | crashlytics-build.properties 48 | fabric.properties 49 | 50 | ### JetBrains Patch ### 51 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 52 | 53 | # *.iml 54 | # modules.xml 55 | # .idea/misc.xml 56 | # *.ipr 57 | 58 | 59 | ### Android ### 60 | # Built application files 61 | *.apk 62 | *.ap_ 63 | 64 | # Files for the ART/Dalvik VM 65 | *.dex 66 | 67 | # Java class files 68 | *.class 69 | 70 | # Generated files 71 | bin/ 72 | gen/ 73 | out/ 74 | 75 | # Gradle files 76 | .gradle/ 77 | build/ 78 | 79 | # Local configuration file (sdk path, etc) 80 | local.properties 81 | 82 | # Proguard folder generated by Eclipse 83 | proguard/ 84 | 85 | # Log Files 86 | *.log 87 | 88 | # Android Studio Navigation editor temp files 89 | .navigation/ 90 | 91 | # Android Studio captures folder 92 | captures/ 93 | 94 | # Intellij 95 | *.iml 96 | .idea/workspace.xml 97 | .idea/libraries 98 | 99 | # Keystore files 100 | *.jks 101 | 102 | ### Android Patch ### 103 | gen-external-apklibs 104 | 105 | 106 | ### Windows ### 107 | # Windows image file caches 108 | Thumbs.db 109 | ehthumbs.db 110 | 111 | # Folder config file 112 | Desktop.ini 113 | 114 | # Recycle Bin used on file shares 115 | $RECYCLE.BIN/ 116 | 117 | # Windows Installer files 118 | *.cab 119 | *.msi 120 | *.msm 121 | *.msp 122 | 123 | # Windows shortcuts 124 | *.lnk 125 | 126 | 127 | ### OSX ### 128 | *.DS_Store 129 | .AppleDouble 130 | .LSOverride 131 | 132 | # Icon must end with two \r 133 | Icon 134 | 135 | 136 | # Thumbnails 137 | ._* 138 | 139 | # Files that might appear in the root of a volume 140 | .DocumentRevisions-V100 141 | .fseventsd 142 | .Spotlight-V100 143 | .TemporaryItems 144 | .Trashes 145 | .VolumeIcon.icns 146 | .com.apple.timemachine.donotpresent 147 | 148 | # Directories potentially created on remote AFP share 149 | .AppleDB 150 | .AppleDesktop 151 | Network Trash Folder 152 | Temporary Items 153 | .apdisk 154 | 155 | 156 | ### Java ### 157 | *.class 158 | 159 | # Mobile Tools for Java (J2ME) 160 | .mtj.tmp/ 161 | 162 | # Package Files # 163 | *.jar 164 | *.war 165 | *.ear 166 | 167 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 168 | hs_err_pid* 169 | 170 | 171 | ### Gradle ### 172 | .gradle 173 | /build/ 174 | 175 | # Ignore Gradle GUI config 176 | gradle-app.setting 177 | 178 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 179 | !gradle-wrapper.jar 180 | 181 | # Cache of project 182 | .gradletasknamecache 183 | 184 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 185 | # gradle/wrapper/gradle-wrapper.properties 186 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk8 3 | 4 | env: 5 | matrix: 6 | - ANDROID_TARGET=android-24 ANDROID_ABI=armeabi-v7a 7 | git: 8 | depth: 10000 9 | 10 | matrix: 11 | fast_finish: true 12 | 13 | android: 14 | components: 15 | - platform-tools 16 | - tools 17 | 18 | # The BuildTools version used by your project 19 | - build-tools-24.0.1 20 | 21 | # The SDK version used to compile your project 22 | - android-24 23 | 24 | # Additional components 25 | - extra-google-google_play_services 26 | - extra-google-m2repository 27 | - extra-android-m2repository 28 | - addon-google_apis-google-24 29 | 30 | licenses: 31 | - android-sdk-license-.+ 32 | 33 | before_install: 34 | - git submodule update --init --recursive 35 | - chmod u+x gradlew 36 | - android list sdk --no-ui --all --extended 37 | - android list targets 38 | 39 | install: true 40 | 41 | script: 42 | - ./gradlew -v 43 | - ./gradlew clean build --stacktrace 44 | 45 | after_failure: true 46 | 47 | notifications: 48 | email: false 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Logging support for Android devices [![](https://jitpack.io/v/LogentriesCommunity/le_android.svg)](https://jitpack.io/#LogentriesCommunity/le_android) [![Build Status](https://travis-ci.org/LogentriesCommunity/le_android.svg)](https://travis-ci.org/LogentriesCommunity/le_android) [![API](https://img.shields.io/badge/API-15%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=15) [![Gradle Version](https://img.shields.io/badge/gradle-3.0-green.svg)](https://docs.gradle.org/current/release-notes) [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/mdp/rotp/blob/master/LICENSE) [![Javadoc](https://img.shields.io/badge/javadoc-SNAPSHOT-green.svg)](https://jitpack.io/com/github/kibotu/le_android/master-SNAPSHOT/javadoc/index.html) 2 | 3 | 4 | Build requirements: Android SDK 2.3+ 5 | 6 | Runtime requirements: Android OS 2.3+ 7 | 8 | Features 9 | -------- 10 | 11 | - Log events are forwarded over TCP to a specific log 12 | 13 | A unique Token UUID for the log is appended to each log event that is sent 14 | 15 | - Send Logs via SSL (only available with the TCP Token method) 16 | 17 | The library can send logs via Token TCP over TLS/SSL to port 443 18 | 19 | Note that the library itself does not validate or manage TLS/SSL certificates! 20 | 21 | - Storing logs offline and sending when connected. 22 | 23 | While sending logs, if the device looses connection, logs are stored locally until a connection is reestablished 24 | 25 | 10mb queue limit 26 | 27 | - TraceID 28 | 29 | Each log event sent contains the device TraceID which is a unique 35 character ID. 30 | 31 | - Datahub support 32 | 33 | Log events can be forward by TCP Token to Datahub 34 | 35 | - Send Logs via HTTP POST (Note that this option will be deprecated in the near future!) 36 | 37 | Option of changing from Token TCP to using HTTP POST sending to the endpoint 'http://js.logentries.com/v1/logs/LOG-TOKEN' 38 | 39 | It is recommended to use the Token TCP default, which also has the option of using TLS/SSL 40 | 41 | Setup 42 | ----- 43 | 44 | Set up an account with Logentries at , and create a logfile, by clicking + Add New button and selecting the Manual Configuration Option at the bottom. Select Token TCP as the source type and copy the Token UUID printed in green. 45 | 46 | Next go to [Jitpack](https://jitpack.io/#LogentriesCommunity/le_android) and select the latest version of the Android library. Follow the instructions provided to install the library. 47 | 48 | Add the permission "android.permission.INTERNET" to the project manifest file. 49 | 50 | Use 51 | --- 52 | 53 | In the desired Activity class, ``import com.logentries.logger.AndroidLogger;`` 54 | 55 | The following simple example shows the library used in a basic Android application Activity - where the logger is set 56 | to use TCP with a Token UUID "159axea4-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - this is a dummy token for demonstration purposes only. 57 | Remember to replace the Token with one taken from the Log created from the earlier setup. 58 | 59 | When a new instance of the Activity is created, a simple log event message is sent. For further information on the Android 60 | Activity and its life cycle, refer to the official Android developer documentation. 61 | 62 | import android.app.Activity; 63 | import android.os.Bundle; 64 | import com.logentries.logger.AndroidLogger; 65 | import java.io.IOException; 66 | 67 | public class MyActivity extends Activity { 68 | private AndroidLogger logger = null; 69 | 70 | /** 71 | * Called when the activity is first created. 72 | */ 73 | @Override 74 | public void onCreate(Bundle savedInstanceState) { 75 | super.onCreate(savedInstanceState); 76 | setContentView(R.layout.main); 77 | try { 78 | logger = AndroidLogger.createInstance(getApplicationContext(), false, false, false, null, 0, "159axea4-xxxx-xxxx-xxxx-xxxxxxxxxxxx", true); 79 | } catch (IOException e) { 80 | e.printStackTrace(); 81 | } 82 | logger.log("MyActivity has been created"); 83 | } 84 | } 85 | 86 | The number and type of arguments of the 'AndroidLogger.createInstance' are as follows: 87 | 88 | (Context context, boolean useHttpPost, boolean useSsl, boolean isUsingDataHub, String dataHubAddr, int dataHubPort, String token, boolean logHostName) 89 | 90 | Note that exceptions are generate where mutually exclusive settings collide - these are: 91 | "useHttpPost" and "useSsl" cannot be both true - HTTP is not available with TLS/SSL 92 | "useHttpPost" and "isUsingDataHub" cannot be both true - use one or the other only 93 | 94 | - 'context' : for example, if in an Activity class, use ``getApplicationContext()``, or if in an Application class, use ``getBaseContext()``. 95 | 96 | - 'useHttpPost' : if set true, use HTTP (note cannot be used with TLS/SSL or the Datahub) 97 | 98 | - 'useSsl' : if set true, the data sent using the default TCP Token, will be done over an SSL Socket 99 | Note that the library itself does not validate or manage TLS/SSL certificates - it will use the default TrustManager 100 | and KeyManager used by the application or host. 101 | 102 | - 'isUsingDataHub' : if set true, library will forward log events to a Datahub (requires Datahub IP Address and Port) 103 | 104 | - 'dataHubAddr' : is a String of the IP Address of your DataHub machine. 105 | 106 | - 'dataHubPort' : is an int of the port number of your incoming connection on your DataHub machine. 107 | The default is port 10000, but this can be changed to any port by altering the /etc/leproxy/leproxyLocal.config file 108 | on your DataHub machine and restarting the leproxy daemon using "sudo service leproxy restart". 109 | 110 | - 'token' : the Token UUID, this is unique to the log to which the log events are sent 111 | This can be copied from the log in the the Logentries Account 112 | 113 | - 'logHostName' : if set true will return host name in log event 114 | 115 | 116 | Development 117 | ----------- 118 | 119 | Build the project into a jar using: 120 | 121 | $ ./gradlew jar 122 | 123 | You can also upload the jar to `bintray` using: 124 | 125 | $ ./gradlew bintrayUpload 126 | 127 | In order to upload to `bintray` you will need to set up some values in a `local.properties` file. 128 | This file should contain your `bintray` information and also your repo settings. 129 | 130 | Once uploaded to `bintray` you should be able to add this library as a dependency as normal using in your `pom.xml` or `build.gradle` file. 131 | More details on which `repo` to include can be found on the `bintray` website. 132 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | maven { url "https://plugins.gradle.org/m2/" } 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:2.2.0-beta2' 8 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | jcenter() 15 | } 16 | } 17 | 18 | task clean(type: Delete) { 19 | delete rootProject.buildDir 20 | } 21 | 22 | task wrapper(type: Wrapper) { 23 | gradleVersion = '3.0' 24 | } 25 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LogentriesCommunity/le_android/df8d98a1c26a78a42a1553fc9071fc3a7e735387/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Aug 26 09:24:29 CEST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.0-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 165 | if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then 166 | cd "$(dirname "$0")" 167 | fi 168 | 169 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 170 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/jetbrains,windows,osx,android 3 | 4 | ### JetBrains ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff: 9 | .idea/workspace.xml 10 | .idea/tasks.xml 11 | .idea/dictionaries 12 | .idea/vcs.xml 13 | .idea/jsLibraryMappings.xml 14 | 15 | # Sensitive or high-churn files: 16 | .idea/dataSources.ids 17 | .idea/dataSources.xml 18 | .idea/dataSources.local.xml 19 | .idea/sqlDataSources.xml 20 | .idea/dynamic.xml 21 | .idea/uiDesigner.xml 22 | 23 | # Gradle: 24 | .idea/gradle.xml 25 | .idea/libraries 26 | 27 | # Mongo Explorer plugin: 28 | .idea/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | /out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Crashlytics plugin (for Android Studio and IntelliJ) 45 | com_crashlytics_export_strings.xml 46 | crashlytics.properties 47 | crashlytics-build.properties 48 | fabric.properties 49 | 50 | ### JetBrains Patch ### 51 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 52 | 53 | # *.iml 54 | # modules.xml 55 | # .idea/misc.xml 56 | # *.ipr 57 | 58 | 59 | ### Windows ### 60 | # Windows image file caches 61 | Thumbs.db 62 | ehthumbs.db 63 | 64 | # Folder config file 65 | Desktop.ini 66 | 67 | # Recycle Bin used on file shares 68 | $RECYCLE.BIN/ 69 | 70 | # Windows Installer files 71 | *.cab 72 | *.msi 73 | *.msm 74 | *.msp 75 | 76 | # Windows shortcuts 77 | *.lnk 78 | 79 | 80 | ### OSX ### 81 | *.DS_Store 82 | .AppleDouble 83 | .LSOverride 84 | 85 | # Icon must end with two \r 86 | Icon 87 | 88 | 89 | # Thumbnails 90 | ._* 91 | 92 | # Files that might appear in the root of a volume 93 | .DocumentRevisions-V100 94 | .fseventsd 95 | .Spotlight-V100 96 | .TemporaryItems 97 | .Trashes 98 | .VolumeIcon.icns 99 | .com.apple.timemachine.donotpresent 100 | 101 | # Directories potentially created on remote AFP share 102 | .AppleDB 103 | .AppleDesktop 104 | Network Trash Folder 105 | Temporary Items 106 | .apdisk 107 | 108 | 109 | ### Android ### 110 | # Built application files 111 | *.apk 112 | *.ap_ 113 | 114 | # Files for the ART/Dalvik VM 115 | *.dex 116 | 117 | # Java class files 118 | *.class 119 | 120 | # Generated files 121 | bin/ 122 | gen/ 123 | out/ 124 | 125 | # Gradle files 126 | .gradle/ 127 | build/ 128 | 129 | # Local configuration file (sdk path, etc) 130 | local.properties 131 | 132 | # Proguard folder generated by Eclipse 133 | proguard/ 134 | 135 | # Log Files 136 | *.log 137 | 138 | # Android Studio Navigation editor temp files 139 | .navigation/ 140 | 141 | # Android Studio captures folder 142 | captures/ 143 | 144 | # Intellij 145 | *.iml 146 | .idea/workspace.xml 147 | .idea/libraries 148 | 149 | # Keystore files 150 | *.jks 151 | 152 | ### Android Patch ### 153 | gen-external-apklibs 154 | -------------------------------------------------------------------------------- /lib/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | 4 | android { 5 | 6 | // apparently javadoc fails using this: 7 | // useLibrary 'org.apache.http.legacy' 8 | 9 | compileSdkVersion 24 10 | buildToolsVersion "24.0.1" 11 | 12 | defaultConfig { 13 | minSdkVersion 15 14 | targetSdkVersion 24 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 19 | 20 | } 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | 28 | lintOptions { 29 | checkReleaseBuilds false 30 | abortOnError false 31 | ignoreWarnings true 32 | disable 'InvalidPackage' 33 | } 34 | } 35 | 36 | dependencies { 37 | compile group: 'org.jbundle.util.osgi.wrapped', name: 'org.jbundle.util.osgi.wrapped.org.apache.http.client', version: '4.1.2' 38 | } 39 | 40 | // build a jar with source files 41 | task sourcesJar(type: Jar) { 42 | from android.sourceSets.main.java.srcDirs 43 | classifier = 'sources' 44 | } 45 | 46 | task javadoc(type: Javadoc) { 47 | failOnError false 48 | source = android.sourceSets.main.java.sourceFiles 49 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 50 | classpath += configurations.compile 51 | } 52 | 53 | // build a jar with javadoc 54 | task javadocJar(type: Jar, dependsOn: javadoc) { 55 | classifier = 'javadoc' 56 | from javadoc.destinationDir 57 | } 58 | 59 | artifacts { 60 | archives sourcesJar 61 | archives javadocJar 62 | } -------------------------------------------------------------------------------- /lib/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in E:\android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/src/main/java/com/logentries/logger/AndroidLogger.java: -------------------------------------------------------------------------------- 1 | package com.logentries.logger; 2 | 3 | import android.content.Context; 4 | 5 | import java.io.IOException; 6 | 7 | public class AndroidLogger { 8 | 9 | private static AndroidLogger instance; 10 | 11 | private AsyncLoggingWorker loggingWorker; 12 | 13 | private AndroidLogger(Context context, boolean useHttpPost, boolean useSsl, boolean isUsingDataHub, String dataHubAddr, int dataHubPort, 14 | String token, boolean logHostName) throws IOException { 15 | loggingWorker = new AsyncLoggingWorker(context, useSsl, useHttpPost, isUsingDataHub, token, dataHubAddr, dataHubPort, logHostName); 16 | } 17 | 18 | public static synchronized AndroidLogger createInstance(Context context, boolean useHttpPost, boolean useSsl, boolean isUsingDataHub, 19 | String dataHubAddr, int dataHubPort, String token, boolean logHostName) 20 | throws IOException { 21 | if (instance != null) { 22 | instance.loggingWorker.close(); 23 | } 24 | 25 | instance = new AndroidLogger(context, useHttpPost, useSsl, isUsingDataHub, dataHubAddr, dataHubPort, token, logHostName); 26 | return instance; 27 | } 28 | 29 | public static synchronized AndroidLogger getInstance() { 30 | if (instance != null) { 31 | return instance; 32 | } else { 33 | throw new IllegalArgumentException("Logger instance is not initialized. Call createInstance() first!"); 34 | } 35 | } 36 | 37 | /** 38 | * Set whether you wish to send your log message without additional meta data to Logentries. 39 | * @param sendRawLogMessage Set to true if you wish to send raw log messages 40 | */ 41 | public void setSendRawLogMessage(boolean sendRawLogMessage){ 42 | loggingWorker.setSendRawLogMessage(sendRawLogMessage); 43 | } 44 | 45 | /** 46 | * Returns whether the logger is configured to send raw log messages or not. 47 | * @return 48 | */ 49 | public boolean getSendRawLogMessage(){ 50 | return loggingWorker.getSendRawLogMessage(); 51 | } 52 | 53 | public void log(String message) { 54 | loggingWorker.addLineToQueue(message); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/main/java/com/logentries/logger/AsyncLoggingWorker.java: -------------------------------------------------------------------------------- 1 | package com.logentries.logger; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import com.logentries.misc.Utils; 7 | import com.logentries.net.LogentriesClient; 8 | 9 | import java.io.IOException; 10 | import java.util.ArrayDeque; 11 | import java.util.Queue; 12 | import java.util.concurrent.ArrayBlockingQueue; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | public class AsyncLoggingWorker { 16 | 17 | /* 18 | * Constants 19 | */ 20 | 21 | private static final String TAG = "LogentriesAndroidLogger"; 22 | 23 | private static final int RECONNECT_WAIT = 100; // milliseconds. 24 | private static final int MAX_QUEUE_POLL_TIME = 1000; // milliseconds. 25 | /** 26 | * Size of the internal event queue. 27 | */ 28 | private static final int QUEUE_SIZE = 32768; 29 | /** 30 | * Limit on individual log length ie. 2^16 31 | */ 32 | public static final int LOG_LENGTH_LIMIT = 65536; 33 | 34 | private static final int MAX_NETWORK_FAILURES_ALLOWED = 3; 35 | private static final int MAX_RECONNECT_ATTEMPTS = 3; 36 | 37 | /** 38 | * Error message displayed when invalid API key is detected. 39 | */ 40 | private static final String INVALID_TOKEN = "Given Token does not look right!"; 41 | 42 | /** 43 | * Error message displayed when queue overflow occurs 44 | */ 45 | private static final String QUEUE_OVERFLOW = "Logentries Buffer Queue Overflow. Message Dropped!"; 46 | 47 | /** 48 | * Indicator if the socket appender has been started. 49 | */ 50 | private boolean started = false; 51 | 52 | /** 53 | * Whether should send logs with or without meta data 54 | */ 55 | private boolean sendRawLogMessage = false; 56 | 57 | /** 58 | * Asynchronous socket appender. 59 | */ 60 | private SocketAppender appender; 61 | 62 | /** 63 | * Message queue. 64 | */ 65 | private ArrayBlockingQueue queue; 66 | 67 | /** 68 | * Logs queue storage 69 | */ 70 | private LogStorage localStorage; 71 | 72 | public AsyncLoggingWorker(Context context, boolean useSsl, boolean useHttpPost, boolean useDataHub, String logToken, 73 | String dataHubAddress, int dataHubPort, boolean logHostName) throws IOException { 74 | 75 | if (!checkTokenFormat(logToken)) { 76 | throw new IllegalArgumentException(INVALID_TOKEN); 77 | } 78 | 79 | queue = new ArrayBlockingQueue(QUEUE_SIZE); 80 | localStorage = new LogStorage(context); 81 | appender = new SocketAppender(useHttpPost, useSsl, useDataHub, dataHubAddress, dataHubPort, logToken, logHostName, this.sendRawLogMessage); 82 | appender.start(); 83 | started = true; 84 | } 85 | 86 | public AsyncLoggingWorker(Context context, boolean useSsl, boolean useHttpPost, String logToken) throws IOException { 87 | this(context, useSsl, useHttpPost, false, logToken, null, 0, true); 88 | } 89 | 90 | public AsyncLoggingWorker(Context context, boolean useSsl, String logToken) throws IOException { 91 | this(context, useSsl, false, false, logToken, null, 0, true); 92 | } 93 | 94 | public AsyncLoggingWorker(Context context, boolean useSsl, String logToken, String dataHubAddr, int dataHubPort) 95 | throws IOException { 96 | this(context, useSsl, false, true, logToken, dataHubAddr, dataHubPort, true); 97 | } 98 | 99 | public void setSendRawLogMessage(boolean sendRawLogMessage){ 100 | this.sendRawLogMessage = sendRawLogMessage; 101 | } 102 | 103 | public boolean getSendRawLogMessage(){ 104 | return sendRawLogMessage; 105 | } 106 | 107 | public void addLineToQueue(String line) { 108 | 109 | // Check that we have all parameters set and socket appender running. 110 | if (!this.started) { 111 | 112 | appender.start(); 113 | started = true; 114 | } 115 | 116 | if (line.length() > LOG_LENGTH_LIMIT) { 117 | for (String logChunk : Utils.splitStringToChunks(line, LOG_LENGTH_LIMIT)) { 118 | tryOfferToQueue(logChunk); 119 | } 120 | 121 | } else { 122 | tryOfferToQueue(line); 123 | } 124 | } 125 | 126 | /** 127 | * Stops the socket appender. queueFlushTimeout (if greater than 0) sets the maximum timeout in milliseconds for 128 | * the message queue to be flushed by the socket appender, before it is stopped. If queueFlushTimeout 129 | * is equal to zero - the method will wait until the queue is empty (which may be dangerous if the 130 | * queue is constantly populated by another thread mantime. 131 | * 132 | * @param queueFlushTimeout - max. wait time in milliseconds for the message queue to be flushed. 133 | */ 134 | public void close(long queueFlushTimeout) { 135 | if (queueFlushTimeout < 0) { 136 | throw new IllegalArgumentException("queueFlushTimeout must be greater or equal to zero"); 137 | } 138 | 139 | long now = System.currentTimeMillis(); 140 | 141 | while (!queue.isEmpty()) { 142 | if (queueFlushTimeout != 0) { 143 | if (System.currentTimeMillis() - now >= queueFlushTimeout) { 144 | // The timeout expired - need to stop the appender. 145 | break; 146 | } 147 | } 148 | } 149 | appender.interrupt(); 150 | started = false; 151 | } 152 | 153 | public void close() { 154 | close(0); 155 | } 156 | 157 | private static boolean checkTokenFormat(String token) { 158 | 159 | return Utils.checkValidUUID(token); 160 | } 161 | 162 | private void tryOfferToQueue(String line) throws RuntimeException { 163 | if (!queue.offer(line)) { 164 | Log.e(TAG, "The queue is full - will try to drop the oldest message in it."); 165 | queue.poll(); 166 | /* 167 | FIXME: This code migrated from LE Java Library; currently, there is no a simple 168 | way to backup the queue in case of overflow due to requirements to max. 169 | memory consumption and max. possible size of the local logs storage. If use 170 | the local storage - the we have three problems: 1) Correct joining of logs from 171 | the queue and from the local storage (and we need some right event to trigger this joining); 172 | 2) Correct order of logs after joining; 3) Data consistence problem, because we're 173 | accessing the storage from different threads, so sync. logic will increase overall 174 | complexity of the code. So, for now this logic is left AS IS, due to relatively 175 | rareness of the case with queue overflow. 176 | */ 177 | 178 | if (!queue.offer(line)) { 179 | throw new RuntimeException(QUEUE_OVERFLOW); 180 | } 181 | } 182 | } 183 | 184 | private class SocketAppender extends Thread { 185 | 186 | // Formatting constants 187 | private static final String LINE_SEP_REPLACER = "\u2028"; 188 | 189 | private LogentriesClient leClient; 190 | 191 | private boolean useHttpPost; 192 | private boolean useSsl; 193 | private boolean isUsingDataHub; 194 | private String dataHubAddr; 195 | private int dataHubPort; 196 | private String token; 197 | private boolean logHostName = true; 198 | private boolean sendRawLogMessage = false; 199 | 200 | public SocketAppender(boolean useHttpPost, boolean useSsl, boolean isUsingDataHub, String dataHubAddr, int dataHubPort, 201 | String token, boolean logHostName, boolean sendRawLogMessage) { 202 | super("Logentries Socket appender"); 203 | 204 | // Don't block shut down 205 | setDaemon(true); 206 | 207 | this.useHttpPost = useHttpPost; 208 | this.useSsl = useSsl; 209 | this.isUsingDataHub = isUsingDataHub; 210 | this.dataHubAddr = dataHubAddr; 211 | this.dataHubPort = dataHubPort; 212 | this.token = token; 213 | this.logHostName = logHostName; 214 | this.sendRawLogMessage = sendRawLogMessage; 215 | } 216 | 217 | private void openConnection() throws IOException, InstantiationException { 218 | if (leClient == null) { 219 | leClient = new LogentriesClient(useHttpPost, useSsl, isUsingDataHub, dataHubAddr, dataHubPort, token); 220 | } 221 | 222 | leClient.connect(); 223 | } 224 | 225 | private boolean reopenConnection(int maxReConnectAttempts) throws InterruptedException, InstantiationException { 226 | if (maxReConnectAttempts < 0) { 227 | throw new IllegalArgumentException("maxReConnectAttempts value must be greater or equal to zero"); 228 | } 229 | 230 | // Close the previous connection 231 | closeConnection(); 232 | 233 | for (int attempt = 0; attempt < maxReConnectAttempts; ++attempt) { 234 | try { 235 | 236 | openConnection(); 237 | return true; 238 | 239 | } catch (IOException e) { 240 | // Ignore the exception and go for the next 241 | // iteration. 242 | } 243 | 244 | Thread.sleep(RECONNECT_WAIT); 245 | } 246 | 247 | return false; 248 | } 249 | 250 | 251 | private void closeConnection() { 252 | if (this.leClient != null) { 253 | this.leClient.close(); 254 | } 255 | } 256 | 257 | private boolean tryUploadSavedLogs() { 258 | Queue logs = new ArrayDeque(); 259 | 260 | try { 261 | 262 | logs = localStorage.getAllLogsFromStorage(false); 263 | for (String msg = logs.peek(); msg != null; msg = logs.peek()) { 264 | if(sendRawLogMessage){ 265 | leClient.write(Utils.formatMessage(msg.replace("\n", LINE_SEP_REPLACER),logHostName, useHttpPost)); 266 | }else{ 267 | leClient.write(msg.replace("\n", LINE_SEP_REPLACER)); 268 | } 269 | logs.poll(); // Remove the message after successful sending. 270 | } 271 | 272 | // All logs have been uploaded - remove the storage file and create the blank one. 273 | try { 274 | localStorage.reCreateStorageFile(); 275 | } catch (IOException ex) { 276 | Log.e(TAG, ex.getMessage()); 277 | } 278 | 279 | return true; 280 | 281 | } catch (IOException ioEx) { 282 | Log.e(TAG, "Cannot upload logs to the server. Error: " + ioEx.getMessage()); 283 | 284 | // Try to save back all messages, that haven't been sent yet. 285 | try { 286 | localStorage.reCreateStorageFile(); 287 | for (String msg : logs) { 288 | localStorage.putLogToStorage(msg); 289 | } 290 | } catch (IOException ioEx2) { 291 | Log.e(TAG, "Cannot save logs to the local storage - part of messages will be " + 292 | "dropped! Error: " + ioEx2.getMessage()); 293 | } 294 | } 295 | 296 | return false; 297 | } 298 | 299 | @Override 300 | public void run() { 301 | try { 302 | 303 | // Open connection 304 | reopenConnection(MAX_RECONNECT_ATTEMPTS); 305 | 306 | Queue prevSavedLogs = localStorage.getAllLogsFromStorage(true); 307 | 308 | int numFailures = 0; 309 | boolean connectionIsBroken = false; 310 | String message = null; 311 | 312 | // Send data in queue 313 | while (true) { 314 | 315 | // First we need to send the logs from the local storage - 316 | // they haven't been sent during the last session, so need to 317 | // come first. 318 | if (prevSavedLogs.isEmpty()) { 319 | 320 | // Try to take data from the queue if there are no logs from 321 | // the local storage left to send. 322 | message = queue.poll(MAX_QUEUE_POLL_TIME, TimeUnit.MILLISECONDS); 323 | 324 | } else { 325 | 326 | // Getting messages from the previous session one by one. 327 | message = prevSavedLogs.poll(); 328 | } 329 | 330 | // Send data, reconnect if needed. 331 | while (true) { 332 | 333 | try { 334 | 335 | // If we have broken connection, then try to re-connect and send 336 | // all logs from the local storage. If succeeded - reset numFailures. 337 | if (connectionIsBroken && reopenConnection(MAX_RECONNECT_ATTEMPTS)) { 338 | if (tryUploadSavedLogs()) { 339 | connectionIsBroken = false; 340 | numFailures = 0; 341 | } 342 | } 343 | 344 | if (message != null) { 345 | this.leClient.write(Utils.formatMessage(message.replace("\n", LINE_SEP_REPLACER), 346 | logHostName, useHttpPost)); 347 | message = null; 348 | } 349 | 350 | } catch (IOException e) { 351 | 352 | if (numFailures >= MAX_NETWORK_FAILURES_ALLOWED) { 353 | connectionIsBroken = true; // Have tried to reconnect for MAX_NETWORK_FAILURES_ALLOWED 354 | // times and failed, so assume, that we have no link to the 355 | // server at all... 356 | try { 357 | // ... and put the current message to the local storage. 358 | localStorage.putLogToStorage(message); 359 | message = null; 360 | } catch (IOException ex) { 361 | Log.e(TAG, "Cannot save the log message to the local storage! Error: " + 362 | ex.getMessage()); 363 | } 364 | 365 | } else { 366 | ++numFailures; 367 | 368 | // Try to re-open the lost connection. 369 | reopenConnection(MAX_RECONNECT_ATTEMPTS); 370 | } 371 | 372 | continue; 373 | } 374 | 375 | break; 376 | } 377 | } 378 | } catch (InterruptedException e) { 379 | // We got interrupted, stop. 380 | 381 | } catch (InstantiationException e) { 382 | Log.e(TAG, "Cannot instantiate LogentriesClient due to improper configuration. Error: " + e.getMessage()); 383 | 384 | // Save all existing logs to the local storage. 385 | // There is nothing we can do else in this case. 386 | String message = queue.poll(); 387 | try { 388 | while (message != null) { 389 | localStorage.putLogToStorage(message); 390 | message = queue.poll(); 391 | } 392 | } catch (IOException ex) { 393 | Log.e(TAG, "Cannot save logs queue to the local storage - all log messages will be dropped! Error: " + 394 | e.getMessage()); 395 | } 396 | } 397 | 398 | closeConnection(); 399 | } 400 | } 401 | 402 | 403 | } 404 | -------------------------------------------------------------------------------- /lib/src/main/java/com/logentries/logger/LogStorage.java: -------------------------------------------------------------------------------- 1 | package com.logentries.logger; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import java.io.BufferedReader; 7 | import java.io.DataInputStream; 8 | import java.io.File; 9 | import java.io.FileInputStream; 10 | import java.io.FileOutputStream; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.util.ArrayDeque; 14 | import java.util.Queue; 15 | 16 | public class LogStorage { 17 | 18 | private static final String TAG = "LogentriesAndroidLogger"; 19 | private static final String STORAGE_FILE_NAME = "LogentriesLogStorage.log"; 20 | private static final long MAX_QUEUE_FILE_SIZE = 10 * 1024 * 1024; // 10 MBytes. 21 | 22 | private Context context; 23 | 24 | private File storageFilePtr = null; // We keep the ptr permanently, because frequently accessing 25 | // the file for retrieving it's size. 26 | 27 | public LogStorage(Context context) throws IOException { 28 | this.context = context; 29 | storageFilePtr = create(); 30 | } 31 | 32 | public void putLogToStorage(String message) throws IOException, RuntimeException { 33 | 34 | // Fix line endings for ingesting the log to the local storage. 35 | if (!message.endsWith("\n")) { 36 | message += "\n"; 37 | } 38 | 39 | FileOutputStream writer = null; 40 | try { 41 | byte[] rawMessage = message.getBytes(); 42 | long currSize = getCurrentStorageFileSize() + rawMessage.length; 43 | String sizeStr = Long.toString(currSize); 44 | Log.d(TAG, "Current size: " + sizeStr); 45 | if (currSize >= MAX_QUEUE_FILE_SIZE) { 46 | Log.d(TAG, "Log storage will be cleared because threshold of " + MAX_QUEUE_FILE_SIZE + " bytes has been reached"); 47 | reCreateStorageFile(); 48 | } 49 | 50 | writer = context.openFileOutput(STORAGE_FILE_NAME, Context.MODE_APPEND); 51 | writer.write(rawMessage); 52 | 53 | } finally { 54 | if (writer != null) { 55 | writer.close(); 56 | } 57 | } 58 | } 59 | 60 | public Queue getAllLogsFromStorage(boolean needToRemoveStorageFile) { 61 | Queue logs = new ArrayDeque(); 62 | FileInputStream input = null; 63 | 64 | try { 65 | input = context.openFileInput(STORAGE_FILE_NAME); 66 | DataInputStream inputStream = new DataInputStream(input); 67 | BufferedReader bufReader = new BufferedReader(new InputStreamReader(inputStream)); 68 | 69 | String logLine = bufReader.readLine(); 70 | while (logLine != null) { 71 | logs.offer(logLine); 72 | logLine = bufReader.readLine(); 73 | } 74 | 75 | if (needToRemoveStorageFile) { 76 | removeStorageFile(); 77 | } 78 | 79 | } catch (IOException ex) { 80 | Log.e(TAG, "Cannot load logs from the local storage: " + ex.getMessage()); 81 | // Basically, ignore the exception - if something has gone wrong - just return empty 82 | // logs list. 83 | } finally { 84 | try { 85 | if (input != null) { 86 | input.close(); 87 | } 88 | } catch (IOException ex2) { 89 | Log.e(TAG, "Cannot close the local storage file: " + ex2.getMessage()); 90 | } 91 | } 92 | 93 | return logs; 94 | } 95 | 96 | public void removeStorageFile() throws IOException { 97 | if (!storageFilePtr.delete()) { 98 | throw new IOException("Cannot delete " + STORAGE_FILE_NAME); 99 | } 100 | } 101 | 102 | public void reCreateStorageFile() throws IOException { 103 | Log.d(TAG, "Log storage has been re-created."); 104 | if (storageFilePtr == null) { 105 | storageFilePtr = create(); 106 | } else { 107 | removeStorageFile(); 108 | } 109 | storageFilePtr = create(); 110 | } 111 | 112 | private File create() throws IOException { 113 | return new File(context.getFilesDir(), STORAGE_FILE_NAME); 114 | } 115 | 116 | private long getCurrentStorageFileSize() throws IOException { 117 | if (storageFilePtr == null) { 118 | storageFilePtr = create(); 119 | } 120 | 121 | return storageFilePtr.length(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/src/main/java/com/logentries/misc/Utils.java: -------------------------------------------------------------------------------- 1 | package com.logentries.misc; 2 | 3 | import android.os.Build; 4 | import android.util.Log; 5 | 6 | import java.lang.reflect.Method; 7 | import java.net.InetAddress; 8 | import java.net.UnknownHostException; 9 | import java.security.MessageDigest; 10 | import java.security.NoSuchAlgorithmException; 11 | import java.util.ArrayList; 12 | import java.util.UUID; 13 | import java.util.regex.Pattern; 14 | 15 | import org.json.JSONArray; 16 | import org.json.JSONException; 17 | import org.json.JSONObject; 18 | 19 | public class Utils { 20 | 21 | private static final String TAG = "LogentriesAndroidLogger"; 22 | 23 | /** 24 | * Reg.ex. that is used to check correctness of HostName if it is defined by user 25 | */ 26 | private static final Pattern HOSTNAME_REGEX = Pattern.compile("[$/\\\"&+,:;=?#|<>_* \\[\\]]"); 27 | 28 | private static String traceID = ""; 29 | private static String hostName = ""; 30 | 31 | // Requires at least API level 9 (v. >= 2.3). 32 | static { 33 | try { 34 | traceID = computeTraceID(); 35 | } catch (NoSuchAlgorithmException ex) { 36 | Log.e(TAG, "Cannot get traceID from device's properties!"); 37 | traceID = "unknown"; 38 | } 39 | 40 | try { 41 | hostName = getProp("net.hostname"); 42 | if (hostName.equals("")) { // We have failed to get the real host name 43 | // so, use the default one. 44 | hostName = InetAddress.getLocalHost().getHostName(); 45 | } 46 | } catch (UnknownHostException e) { 47 | // We cannot resolve local host name - so won't use it at all. 48 | } 49 | } 50 | 51 | private static String getProp(String propertyName) { 52 | 53 | if (propertyName == null || propertyName.isEmpty()) { 54 | return ""; 55 | } 56 | 57 | try { 58 | Method getString = Build.class.getDeclaredMethod("getString", String.class); 59 | getString.setAccessible(true); 60 | return getString.invoke(null, propertyName).toString(); 61 | } catch (Exception ex) { 62 | // Ignore the exception - we simply couldn't access the property; 63 | Log.e(TAG, ex.getMessage()); 64 | } 65 | 66 | return ""; 67 | } 68 | 69 | private static String computeTraceID() throws NoSuchAlgorithmException { 70 | 71 | String fingerprint = getProp("ro.build.fingerprint"); 72 | String displayId = getProp("ro.build.display.id"); 73 | String hardware = getProp("ro.hardware"); 74 | String device = getProp("ro.product.device"); 75 | String rilImei = getProp("ril.IMEI"); 76 | 77 | MessageDigest hashGen = MessageDigest.getInstance("MD5"); 78 | byte[] digest = null; 79 | if (fingerprint.isEmpty() & displayId.isEmpty() & hardware.isEmpty() & device.isEmpty() & rilImei.isEmpty()) { 80 | Log.e(TAG, "Cannot obtain any of device's properties - will use default Trace ID source."); 81 | 82 | Double randomTrace = Math.random() + Math.PI; 83 | String defaultValue = randomTrace.toString(); 84 | randomTrace = Math.random() + Math.PI; 85 | defaultValue += randomTrace.toString().replace(".", ""); 86 | // The code below fixes one strange bug, when call to a freshly installed app crashes at this 87 | // point, because random() produces too short sequence. Note, that this behavior does not 88 | // occur for the second and all further launches. 89 | defaultValue = defaultValue.length() >= 36 ? defaultValue.substring(2, 34) : 90 | defaultValue.substring(2); 91 | 92 | hashGen.update(defaultValue.getBytes()); 93 | } else { 94 | StringBuilder sb = new StringBuilder(); 95 | sb.append(fingerprint).append(displayId).append(hardware).append(device).append(rilImei); 96 | hashGen.update(sb.toString().getBytes()); 97 | } 98 | 99 | digest = hashGen.digest(); 100 | StringBuilder conv = new StringBuilder(); 101 | for (byte b : digest) { 102 | conv.append(String.format("%02x", b & 0xff).toUpperCase()); 103 | } 104 | 105 | return conv.toString(); 106 | } 107 | 108 | public static String getTraceID() { 109 | return traceID; 110 | } 111 | 112 | 113 | private static String getFormattedDeviceId(boolean toJSON) { 114 | if (toJSON) { 115 | return "\"DeviceId\": \"" + Build.SERIAL + "\""; 116 | } 117 | return "DeviceId=" + Build.SERIAL; 118 | } 119 | 120 | public static String getFormattedTraceID(boolean toJSON) { 121 | if (toJSON) { 122 | return "\"TraceID\": \"" + traceID + "\""; 123 | } 124 | return "TraceID=" + traceID; 125 | } 126 | 127 | public static String getHostName() { 128 | return hostName; 129 | } 130 | 131 | public static String getFormattedHostName(boolean toJSON) { 132 | if (toJSON) { 133 | return "\"Host\": \"" + hostName + "\""; 134 | } 135 | return "Host=" + hostName; 136 | } 137 | 138 | /** 139 | * Via http://stackoverflow.com/a/10174938 140 | */ 141 | public static boolean isJSONValid(String message) { 142 | try { 143 | new JSONObject(message); 144 | } catch (JSONException ex) { 145 | try { 146 | new JSONArray(message); 147 | } catch (JSONException ex1) { 148 | return false; 149 | } 150 | } 151 | return true; 152 | } 153 | 154 | /** 155 | * Formats given message to make it suitable for ingestion by Logentris endpoint. 156 | * If isUsingHttp == true, the method produces such structure: 157 | * {"event": {"Host": "SOMEHOST", "Timestamp": 12345, "DeviceID": "DEV_ID", "Message": "MESSAGE"}} 158 | *

159 | * If isUsingHttp == false the output will be like this: 160 | * Host=SOMEHOST Timestamp=12345 DeviceID=DEV_ID MESSAGE 161 | * 162 | * @param message Message to be sent to Logentries 163 | * @param logHostName - if set to true - "Host"=HOSTNAME parameter is appended to the message. 164 | * @param isUsingHttp will be using http 165 | * @return 166 | */ 167 | public static String formatMessage(String message, boolean logHostName, boolean isUsingHttp) { 168 | StringBuilder sb = new StringBuilder(); 169 | 170 | if (isUsingHttp) { 171 | // Add 'event' structure. 172 | sb.append("{\"event\": {"); 173 | } 174 | 175 | if (logHostName) { 176 | sb.append(Utils.getFormattedHostName(isUsingHttp)); 177 | sb.append(isUsingHttp ? ", " : " "); 178 | } 179 | 180 | sb.append(Utils.getFormattedTraceID(isUsingHttp)).append(" "); 181 | sb.append(isUsingHttp ? ", " : " "); 182 | 183 | 184 | sb.append(Utils.getFormattedDeviceId(isUsingHttp)).append(" "); 185 | sb.append(isUsingHttp ? ", " : " "); 186 | 187 | long timestamp = System.currentTimeMillis(); // Current time in UTC in milliseconds. 188 | if (isUsingHttp) { 189 | sb.append("\"Timestamp\": ").append(Long.toString(timestamp)).append(", "); 190 | } else { 191 | sb.append("Timestamp=").append(Long.toString(timestamp)).append(" "); 192 | } 193 | 194 | // Append the event data 195 | if (isUsingHttp) { 196 | if (Utils.isJSONValid(message)) { 197 | sb.append("\"Message\":").append(message); 198 | sb.append("}}"); 199 | } else { 200 | sb.append("\"Message\": \"").append(message); 201 | sb.append("\"}}"); 202 | } 203 | 204 | } else { 205 | sb.append(message); 206 | } 207 | 208 | return sb.toString(); 209 | } 210 | 211 | public static boolean checkValidUUID(String uuid) { 212 | if (uuid != null && !uuid.isEmpty()) { 213 | try { 214 | 215 | UUID u = UUID.fromString(uuid); 216 | return true; 217 | 218 | } catch (IllegalArgumentException e) { 219 | return false; 220 | } 221 | } 222 | return false; 223 | } 224 | 225 | public static boolean checkIfHostNameValid(String hostName) { 226 | return !HOSTNAME_REGEX.matcher(hostName).find(); 227 | } 228 | 229 | public static String[] splitStringToChunks(String source, int chunkLength) { 230 | if (chunkLength < 0) { 231 | throw new IllegalArgumentException("Chunk length must be greater or equal to zero!"); 232 | } 233 | 234 | int srcLength = source.length(); 235 | if (chunkLength == 0 || srcLength <= chunkLength) { 236 | return new String[]{source}; 237 | } 238 | 239 | ArrayList chunkBuffer = new ArrayList(); 240 | int splitSteps = srcLength / chunkLength + (srcLength % chunkLength > 0 ? 1 : 0); 241 | 242 | int lastCutPosition = 0; 243 | for (int i = 0; i < splitSteps; ++i) { 244 | 245 | if (i < splitSteps - 1) { 246 | // Cut out the chunk of the requested size. 247 | chunkBuffer.add(source.substring(lastCutPosition, lastCutPosition + chunkLength)); 248 | } else { 249 | // Cut out all that left to the end of the string. 250 | chunkBuffer.add(source.substring(lastCutPosition)); 251 | } 252 | 253 | lastCutPosition += chunkLength; 254 | } 255 | 256 | return chunkBuffer.toArray(new String[chunkBuffer.size()]); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /lib/src/main/java/com/logentries/net/LogentriesClient.java: -------------------------------------------------------------------------------- 1 | package com.logentries.net; 2 | 3 | import org.apache.http.client.HttpClient; 4 | import org.apache.http.client.HttpResponseException; 5 | import org.apache.http.client.methods.HttpPost; 6 | import org.apache.http.entity.StringEntity; 7 | import org.apache.http.impl.client.DefaultHttpClient; 8 | 9 | import java.io.IOException; 10 | import java.io.OutputStream; 11 | import java.net.Socket; 12 | import java.nio.charset.Charset; 13 | 14 | import javax.net.ssl.SSLSocket; 15 | import javax.net.ssl.SSLSocketFactory; 16 | 17 | import android.util.Log; 18 | 19 | public class LogentriesClient { 20 | // Logentries server endpoints for logs data. 21 | private static final String LE_TOKEN_API = "data.logentries.com"; // For token-based stream input 22 | 23 | private static final String LE_HTTP_API = "http://webhook.logentries.com/noformat/logs/"; // For HTTP-based input. 24 | private static final String LE_HTTPS_API = "https://webhook.logentries.com/noformat/logs/"; // For HTTP-based input. 25 | 26 | // Port number for unencrypted HTTP PUT/Token TCP logging on Logentries server. 27 | private static final int LE_PORT = 80; 28 | 29 | // Port number for SSL HTTP PUT/TLS Token TCP logging on Logentries server. 30 | private static final int LE_SSL_PORT = 443; 31 | 32 | static final Charset UTF8 = Charset.forName("UTF-8"); 33 | 34 | private final SSLSocketFactory sslFactory; 35 | 36 | private Socket socket; // The socket, connected to the Token API endpoint (Token-based input only!) 37 | private OutputStream stream; // Data stream to the endpoint, where log messages go (Token-based input only!) 38 | 39 | private HttpClient httpClient; // HTTP client, used for communicating with HTTP API endpoint. 40 | private HttpPost postRequest; // Request object, used to forward data put requests. 41 | 42 | private String endpointToken; // Token, that points to the exact endpoint - the log object, where the data goes. 43 | 44 | private boolean sslChoice = false; // Use SSL layering for the Socket? 45 | private boolean httpChoice = false; // Use HTTP input instead of token-based stream input? 46 | 47 | // Datahub-related attributes. 48 | private String dataHubServer = null; 49 | private int dataHubPort = 0; 50 | private boolean useDataHub = false; 51 | 52 | // The formatter used to prepend logs with the endpoint token for Token-based input. 53 | private StringBuilder streamFormatter = new StringBuilder(); 54 | 55 | public LogentriesClient(boolean useHttpPost, boolean useSsl, boolean isUsingDataHub, String server, int port, 56 | String token) 57 | throws InstantiationException, IllegalArgumentException { 58 | 59 | if (useHttpPost && isUsingDataHub) { 60 | throw new IllegalArgumentException("'httpPost' parameter cannot be set to true if 'isUsingDataHub' " + 61 | "is set to true."); 62 | } 63 | 64 | if (token == null || token.isEmpty()) { 65 | throw new IllegalArgumentException("Token parameter cannot be empty!"); 66 | } 67 | 68 | useDataHub = isUsingDataHub; 69 | sslChoice = useSsl; 70 | httpChoice = useHttpPost; 71 | endpointToken = token; 72 | 73 | if (useDataHub) { 74 | if (server == null || server.isEmpty()) { 75 | throw new InstantiationException("'server' parameter is mandatory if 'isUsingDatahub' parameter " + 76 | "is set to true."); 77 | } 78 | if (port <= 0 || port > 65535) { 79 | throw new InstantiationException("Incorrect port number " + Integer.toString(port) + ". Port number must " + 80 | "be greater than zero and less than 65535."); 81 | } 82 | dataHubServer = server; 83 | dataHubPort = port; 84 | } 85 | if (useSsl) { 86 | try { 87 | sslFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); 88 | } catch (Exception e) { 89 | throw new InstantiationException("Cannot create LogentriesClient instance. Error: " + e.getMessage()); 90 | } 91 | } else { 92 | sslFactory = null; 93 | } 94 | } 95 | 96 | public int getPort() { 97 | if (useDataHub) { 98 | return dataHubPort; 99 | } else { 100 | return sslChoice ? LE_SSL_PORT : LE_PORT; 101 | } 102 | } 103 | 104 | public String getAddress() { 105 | if (useDataHub) { 106 | return dataHubServer; 107 | } else { 108 | if (httpChoice) { 109 | return sslChoice ? LE_HTTPS_API : LE_HTTP_API; 110 | } 111 | return LE_TOKEN_API; 112 | } 113 | } 114 | 115 | public void connect() throws IOException, IllegalArgumentException { 116 | if (httpChoice) { 117 | httpClient = new DefaultHttpClient(); 118 | postRequest = new HttpPost(getAddress() + endpointToken); 119 | } else { 120 | Socket s = new Socket(getAddress(), getPort()); 121 | if (sslChoice) { 122 | if (sslFactory == null) { 123 | throw new IllegalArgumentException("SSL Socket Factory is not initialized!"); 124 | } 125 | SSLSocket sslSocket = (SSLSocket) sslFactory.createSocket(s, getAddress(), getPort(), true); 126 | sslSocket.setTcpNoDelay(true); 127 | socket = sslSocket; 128 | } else { 129 | socket = s; 130 | } 131 | stream = socket.getOutputStream(); 132 | } 133 | } 134 | 135 | public void write(String data) throws IOException { 136 | if (!httpChoice) { 137 | // Token-based or DataHub output mode - we're using plain stream forwarding via the socket. 138 | if (stream == null) { 139 | throw new IOException("OutputStream is not initialized!"); 140 | } 141 | streamFormatter.setLength(0); // Erase all previous data. 142 | streamFormatter.append(endpointToken).append(" "); 143 | streamFormatter.append(data); 144 | // For Token-based input it is mandatory for the message to has '\n' at the end to be 145 | // ingested by the endpoint correctly. 146 | if (!data.endsWith("\n")) { 147 | streamFormatter.append("\n"); 148 | } 149 | stream.write(streamFormatter.toString().getBytes(UTF8)); 150 | stream.flush(); 151 | } else { 152 | // HTTP input mode. 153 | postRequest.setEntity(new StringEntity(data, "UTF8")); 154 | try { 155 | httpClient.execute(postRequest); 156 | } catch (HttpResponseException ex) { 157 | Log.e("LogentriesAndroidLogger", "Received status code:" + ex.getStatusCode()); 158 | Log.e("LogentriesAndroidLogger", "Error message:" + ex.getMessage()); 159 | } 160 | } 161 | } 162 | 163 | public void close() { 164 | try { 165 | if (socket != null) { 166 | socket.close(); 167 | socket = null; 168 | } 169 | } catch (Exception e) { 170 | // Just hide the exception - we cannot throw here. 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':lib' 2 | --------------------------------------------------------------------------------