├── .gitignore ├── README.md ├── apks └── log-rocket.apk ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── dream │ │ └── logrocket │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── dream │ │ │ └── logrocket │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── shape_grey.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── icon_logo.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── dream │ └── logrocket │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── htmls └── index.html ├── images ├── app_cut.jpg ├── icon_logo.png └── index_info.png ├── log-rocket ├── .gitignore ├── README.MD ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cn │ │ └── net │ │ └── yto │ │ └── logrocket │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── cn │ │ └── net │ │ └── yto │ │ └── logrocket │ │ ├── LogRocket.java │ │ ├── callback │ │ └── UploadLogCallback.java │ │ ├── impl │ │ └── WebSocketServerImpl.java │ │ ├── lifecycle │ │ └── ActivityLifecycleCallbackImpl.java │ │ ├── meta │ │ └── MetaParseUtil.kt │ │ ├── provider │ │ ├── ContentProviderImpl.kt │ │ └── LogRocketProvider.java │ │ ├── refect │ │ └── ReflectLogRocket.java │ │ └── utils │ │ ├── LogCatUtil.java │ │ └── NetworkUtils.java │ └── test │ └── java │ └── cn │ └── net │ └── yto │ └── logrocket │ └── ExampleUnitTest.kt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | .idea 17 | /.idea/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LogRocket 2 | 3 | 4 | 5 | > 好久没发布jitpack了,失败了好几次,导致版本直接干到了v1.0.5,成功后又把之前的删除了,目前只保留了v1.0.1,上面这个图片不想删除,以后迭代上去了,就会保持同步了 6 | 7 | ### 版本 8 | - v1.0.1 「2024-12-06」 9 | 10 |
11 | 12 |
13 | 14 | ### 简介 15 | 当前开发者可在AS控制台查看日志,或者通过adb查看日志,对于开发大有帮助;发生异常也可通过日志上报到日志平台去查看,十分便捷; 16 | 17 | LogRocket换了一个角度,从测试者的角度出发; 18 | 19 | 开发者应该遇到过一些情况: 20 | - 接口数据不对,测试人员直接拿着手机找过来,说报错了,好一点的可能会自己会抓包查看网络接口;不好的就不说了。 21 | - 一些日志崩溃也是如此,测着测着,突然闪退,接了日志系统还好,还能去查看到底什么原因导致的,如果没接入,则需要测试人员反复的去猜测复现,十分不便; 22 | - 接口出了问题,后端找测试要参数,然后测试自己去抓包或者去找移动端开发; 23 | - .... 24 | 25 | > 真想给每个测试人员安装一个AS 26 | 27 | 整个就是基于这种小处境出发,解决测试人员查看日志的问题; 28 | 29 | LogRocket基于此出发,将终端设备作为Server,直接将logcat的日志通过ws直接发送出来,开发者可在日志中加入网络接口打印,以及一些日常打印,测试人员可直接访问对应的ws地址就可直接阅读网络请求以及崩溃日志。 30 | 31 | 可第一时间发现问题并保留日志信息及时反馈,减少中间流转时间,提升效率。 32 | 33 | ### 对代码的影响 34 | LogRocket建议仅仅在开发环境中使用,不建议上生产环境,以免引发不必要的错误; 35 | 36 | 为了实现这种解耦,于是将WebSocket得开启和关闭放在了Provider中,然后通过「debugImplementation」得方式进行依赖,这样在生产环境自动剔除,避免了对生产环境的干扰。 37 | 38 | 另外,终端既然作为Server端,那他就得提供一个ws地址,开发者可直接在wifi链接页面去查看自己手机的ip;通过LogRocket也提供了方法去获取对应终端的ip。 39 | 40 | 端口号是采用随机分配的空闲端口号,因为一个人可能会负责多个应用的开发,同一台设备,同一个网络下,不能同一个端口,故每次生成的ws链接端口号都不同。 41 | 42 | 这里LogRocket也提供的方法来获取整个ws的访问地址; 43 | 44 | 于是这里就会有一个问题,既然是采用「debugImplementation」得方式进行依赖,那肯定不能直接使用LogRocket的方法,不然在生产环境会因为没有依赖找不到对应的Class而报错,所以这里又提供了一个类,专门用于反射获取ws地址的。 45 | 46 | 对于知负责一个应用的开发者,如果会觉得每次更换端口显得很麻烦,想固定端口,这个也是提供了解决办法,可以在AndroidManifest.xml中配置; 47 | 在application节点中加入以下配置(默认值为false): 48 | ```xml 49 | 52 | ``` 53 | 54 | 这里又有了一个问题,ws有了,难道还要找个ws测试网站去看日志?这个可以开发者自行实现,但是LogRocket也提供了一个默认h5页面,可以直接去查看,对应的文件为index.html。 55 | 56 | 这个index.html也是我去找ChatGpt生成的,还带了日志过滤,方便使用者过滤日志。如果您觉得UI不太好看,或者功能过于简陋,可以自行找ChatGpt去定制生成,或者让前端同学帮忙搞搞。 57 | 58 | 因为就只是临时起意的一个小工具,所以就不想去搭服务器放网页,所以直接把h5文件放到了github上,或者直接通过浏览器访问github上的这个index.html即可。 59 | 60 | [日志查看](https://htmlpreview.github.io/?https://github.com/xieyang94/LogRocket/blob/dev/htmls/index.html) 61 | 62 | 对于index.html,输入ws链接,回车即可; 63 | 64 | ![index_info.png](https://github.com/xieyang94/LogRocket/blob/dev/images/index_info.png) 65 | 66 | ### 注意点 67 | 需要保持ws的Server和Client在同一网络 68 | 69 | ### 接入使用方式 70 | 71 | #### 仓库引入 72 | ```gradle 73 | repositories { 74 | maven { url = uri("https://jitpack.io") } 75 | google() 76 | mavenCentral() 77 | } 78 | ``` 79 | #### 依赖引入 80 | ```gradle 81 | debugImplementation 'com.logrocket:logrocket:lastVersion' 82 | ``` 83 | #### 固定端口配置 84 | 默认非固定端口(false) 85 | ```xml 86 | 89 | ``` 90 | 91 | ### 反射获取ws链接 92 | ```java 93 | class Test { 94 | 95 | public String getWsAddress(Context context) { 96 | String result = null; // 默认值,如果反射调用失败则返回此值 97 | try { 98 | // 获取MetaUtil类的Class对象 99 | Class metaUtilClass = Class.forName("cn.net.yto.logrocket.refect.ReflectLogRocket"); 100 | // 获取getPort方法的Method对象 101 | Method getPortMethod = metaUtilClass.getMethod("wdAddress", Context.class); 102 | // 调用getPort方法 103 | // 注意:如果getPort是非公开方法,可能需要设置accessible为true 104 | getPortMethod.setAccessible(true); 105 | // 创建MetaUtil类的实例 106 | Object metaUtilInstance = metaUtilClass.newInstance(); 107 | // 调用invoke方法执行getPort(context) 108 | result = (String) getPortMethod.invoke(metaUtilInstance, context); 109 | } catch (Exception e) { 110 | e.printStackTrace(); 111 | } 112 | return result; 113 | } 114 | } 115 | ``` 116 | 被反射的类: 117 | ```java 118 | public class ReflectLogRocket { 119 | 120 | /** 121 | * 反射获取ws地址 122 | * 123 | * @param context 124 | * @return 125 | */ 126 | public String wdAddress(Context context) { 127 | return LogRocket.getInstance().getWsAddress(context); 128 | } 129 | } 130 | ``` 131 | 您也可以不采用这种用法,可以直接把代码拷贝到您的项目中,然后直接调用。 132 | 133 | ### Demo 134 | Demo样式 135 | ![app_cut.png](https://github.com/xieyang94/LogRocket/blob/dev/images/app_cut.jpg) 136 | 137 | > 可下载apks目录下的apk体验下,在github上的index.html进行ws的访问好像存在点问题,后面有时间在处理,可以把htmls下的index.html下载下来本地打开使用 138 | 139 | ### 其他 140 | 141 | 有问题可以反馈,我会尽最大的努力解决或采纳。 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /apks/log-rocket.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xieyang94/LogRocket/37f531d8c244abd63f99f9efa355c6e41d983a5a/apks/log-rocket.apk -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdkVersion 34 7 | 8 | defaultConfig { 9 | applicationId "com.dream.logrocket" 10 | minSdkVersion 17 11 | targetSdkVersion 34 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | } 29 | 30 | dependencies { 31 | 32 | implementation 'androidx.core:core-ktx:1.6.0' 33 | implementation 'androidx.appcompat:appcompat:1.3.0' 34 | implementation 'com.google.android.material:material:1.4.0' 35 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 36 | testImplementation 'junit:junit:4.13.2' 37 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 38 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 39 | 40 | implementation 'androidx.core:core-ktx:1.3.2' 41 | implementation 'androidx.activity:activity-ktx:1.3.0' 42 | implementation 'androidx.fragment:fragment-ktx:1.3.6' 43 | 44 | 45 | //debugImplementation project(path: ':log-rocket') 46 | debugImplementation 'com.github.xieyang94.LogRocket:log-rocket:v1.0.1' 47 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/dream/logrocket/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.dream.logrocket; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.dream.logrocket", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/dream/logrocket/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.dream.logrocket; 2 | 3 | import android.content.ClipData; 4 | import android.content.ClipboardManager; 5 | import android.content.Context; 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | import android.view.View; 9 | import android.widget.Button; 10 | import android.widget.TextView; 11 | import android.widget.Toast; 12 | 13 | import androidx.appcompat.app.AppCompatActivity; 14 | import androidx.core.widget.NestedScrollView; 15 | 16 | import java.io.BufferedReader; 17 | import java.io.IOException; 18 | import java.io.InputStreamReader; 19 | import java.lang.reflect.Method; 20 | import java.text.SimpleDateFormat; 21 | import java.util.Date; 22 | import java.util.concurrent.CopyOnWriteArrayList; 23 | import java.util.concurrent.ExecutorService; 24 | import java.util.concurrent.Executors; 25 | import java.util.concurrent.atomic.AtomicBoolean; 26 | 27 | public class MainActivity extends AppCompatActivity { 28 | 29 | private static final String TAG = "MainActivity"; 30 | private NestedScrollView scrollView; 31 | private TextView mTvWsAddress; 32 | private TextView tv_log; 33 | private Button mBtnConnect; 34 | private Button mBtnDisconnect; 35 | private Button mBtnTestStart; 36 | private Button mBtnTestStop; 37 | 38 | private ExecutorService executorService; 39 | private final AtomicBoolean interrupt = new AtomicBoolean(false); 40 | 41 | @Override 42 | protected void onCreate(Bundle savedInstanceState) { 43 | super.onCreate(savedInstanceState); 44 | setContentView(R.layout.activity_main); 45 | 46 | initView(); 47 | initListener(); 48 | 49 | String wsAddress = getWsAddress(this); 50 | if (wsAddress != null) { 51 | mTvWsAddress.setText(wsAddress); 52 | } 53 | } 54 | 55 | private void initView() { 56 | scrollView = (NestedScrollView) findViewById(R.id.scrollView); 57 | mTvWsAddress = (TextView) findViewById(R.id.tv_ws_address); 58 | tv_log = (TextView) findViewById(R.id.tv_log); 59 | mBtnConnect = (Button) findViewById(R.id.btn_connect); 60 | mBtnDisconnect = (Button) findViewById(R.id.btn_disconnect); 61 | mBtnTestStart = (Button) findViewById(R.id.btn_test_start); 62 | mBtnTestStop = (Button) findViewById(R.id.btn_test_stop); 63 | 64 | executorService = Executors.newCachedThreadPool(); 65 | 66 | executorService.execute(() -> { 67 | CopyOnWriteArrayList contentList = new CopyOnWriteArrayList<>(); 68 | uploadLog(new Callback() { 69 | @Override 70 | public void upload(String log) { 71 | if (contentList.size() > 30) { 72 | contentList.remove(0); 73 | } 74 | contentList.add(log); 75 | String contentStr = ""; 76 | for (String content : contentList) { 77 | contentStr += content + "\n"; 78 | } 79 | String finalContentStr = contentStr; 80 | runOnUiThread(() -> { 81 | tv_log.setText(finalContentStr); 82 | scrollView.fullScroll(View.FOCUS_DOWN); 83 | }); 84 | } 85 | }); 86 | }); 87 | 88 | } 89 | 90 | public String getWsAddress(Context context) { 91 | String result = null; // 默认值,如果反射调用失败则返回此值 92 | try { 93 | // 获取MetaUtil类的Class对象 94 | Class metaUtilClass = Class.forName("cn.net.yto.logrocket.refect.ReflectLogRocket"); 95 | // 获取getPort方法的Method对象 96 | Method getPortMethod = metaUtilClass.getMethod("wdAddress", Context.class); 97 | // 调用getPort方法 98 | // 注意:如果getPort是非公开方法,可能需要设置accessible为true 99 | getPortMethod.setAccessible(true); 100 | // 创建MetaUtil类的实例 101 | Object metaUtilInstance = metaUtilClass.newInstance(); 102 | // 调用invoke方法执行getPort(context) 103 | result = (String) getPortMethod.invoke(metaUtilInstance, context); 104 | } catch (Exception e) { 105 | e.printStackTrace(); 106 | } 107 | return result; 108 | } 109 | 110 | private void copyText(TextView view) { 111 | // 创建一个剪贴板管理器 112 | ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); 113 | // 创建一个剪贴数据集 114 | ClipData clip = ClipData.newPlainText("label", view.getText()); 115 | // 将剪贴数据集放到剪贴板 116 | clipboard.setPrimaryClip(clip); 117 | // 显示一个短暂的提示,表明复制成功 118 | Toast.makeText(getApplicationContext(), "已复制到剪贴板", Toast.LENGTH_SHORT).show(); 119 | } 120 | 121 | private void initListener() { 122 | mTvWsAddress.setOnClickListener(new View.OnClickListener() { 123 | @Override 124 | public void onClick(View v) { 125 | copyText(mTvWsAddress); 126 | } 127 | }); 128 | mBtnTestStart.setOnClickListener(v -> { 129 | testLog(); 130 | }); 131 | mBtnTestStop.setOnClickListener(v -> { 132 | interrupt.set(false); 133 | }); 134 | } 135 | 136 | private void testLog() { 137 | interrupt.set(true); 138 | executorService.execute(() -> { 139 | try { 140 | while (interrupt.get()) { 141 | try { 142 | Thread.sleep(1000); 143 | Log.e("ccer", currentTime() + "----请注意,这里是测试日志,产生频率为1秒钟一条"); 144 | } catch (Exception e) { 145 | e.printStackTrace(); 146 | } 147 | } 148 | } catch (Exception e) { 149 | e.printStackTrace(); 150 | } 151 | }); 152 | } 153 | 154 | public void uploadLog(Callback callback) { 155 | try { 156 | // 执行logcat命令 157 | Process process = Runtime.getRuntime().exec("logcat"); 158 | BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream())); 159 | String line = ""; 160 | while ((line = bufferedReader.readLine()) != null) { 161 | if (callback != null) { 162 | callback.upload(line); 163 | } 164 | } 165 | // 关闭流 166 | bufferedReader.close(); 167 | } catch (IOException e) { 168 | e.printStackTrace(); 169 | } 170 | } 171 | 172 | interface Callback { 173 | void upload(String log); 174 | } 175 | 176 | private String currentTime() { 177 | try { 178 | // 创建一个Date对象,它将包含当前日期和时间 179 | Date now = new Date(); 180 | // 创建一个SimpleDateFormat对象,用于指定输出格式 181 | SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 182 | // 使用format方法将Date对象格式化为字符串 183 | String currentDateTime = dateFormat.format(now); 184 | return currentDateTime; 185 | } catch (Exception e) { 186 | e.printStackTrace(); 187 | } 188 | return System.currentTimeMillis() + ""; 189 | } 190 | 191 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_grey.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 26 | 27 | 40 | 41 | 47 | 48 |