@onekeyfe/react-native-native-logger 1.1.27 → 1.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ package com.margelo.nitro.nativelogger
3
3
  import com.facebook.proguard.annotations.DoNotStrip
4
4
  import com.margelo.nitro.core.Promise
5
5
  import java.io.File
6
+ import java.io.RandomAccessFile
6
7
 
7
8
  @DoNotStrip
8
9
  class NativeLogger : HybridNativeLoggerSpec() {
@@ -24,24 +25,6 @@ class NativeLogger : HybridNativeLoggerSpec() {
24
25
  Regex("(?:eyJ|AAAA)[A-Za-z0-9+/=]{40,}"),
25
26
  )
26
27
 
27
- /** Rate limiting: max messages per second */
28
- private const val MAX_MESSAGES_PER_SECOND = 100
29
- @Volatile private var messageCount = 0
30
- @Volatile private var windowStartMs = System.currentTimeMillis()
31
- private val rateLimitLock = Any()
32
-
33
- private fun isRateLimited(): Boolean {
34
- synchronized(rateLimitLock) {
35
- val now = System.currentTimeMillis()
36
- if (now - windowStartMs >= 1000L) {
37
- windowStartMs = now
38
- messageCount = 0
39
- }
40
- messageCount++
41
- return messageCount > MAX_MESSAGES_PER_SECOND
42
- }
43
- }
44
-
45
28
  fun sanitize(message: String): String {
46
29
  var result = message
47
30
  for (pattern in sensitivePatterns) {
@@ -54,7 +37,6 @@ class NativeLogger : HybridNativeLoggerSpec() {
54
37
  }
55
38
 
56
39
  override fun write(level: Double, msg: String) {
57
- if (isRateLimited()) return
58
40
  val sanitized = sanitize(msg)
59
41
  when (level.toInt()) {
60
42
  0 -> OneKeyLog.debug("JS", sanitized)
@@ -88,6 +70,7 @@ class NativeLogger : HybridNativeLoggerSpec() {
88
70
 
89
71
  override fun deleteLogFiles(): Promise<Unit> {
90
72
  return Promise.async {
73
+ OneKeyLog.flush()
91
74
  val dir = OneKeyLog.logsDirectory
92
75
  if (dir.isEmpty()) return@async
93
76
  val files = File(dir).listFiles { _, name -> name.endsWith(".log") }
@@ -95,9 +78,16 @@ class NativeLogger : HybridNativeLoggerSpec() {
95
78
  OneKeyLog.warn("NativeLogger", "Failed to list log directory for deletion")
96
79
  return@async
97
80
  }
98
- // Skip the active log file to avoid breaking logback's open file handle
99
- files.filter { it.name != "app-latest.log" }.forEach { file ->
100
- if (!file.delete()) {
81
+ files.forEach { file ->
82
+ try {
83
+ if (file.name == "app-latest.log") {
84
+ RandomAccessFile(file, "rw").use { raf ->
85
+ raf.setLength(0L)
86
+ }
87
+ } else if (!file.delete()) {
88
+ OneKeyLog.warn("NativeLogger", "Failed to delete log file")
89
+ }
90
+ } catch (_: Exception) {
101
91
  OneKeyLog.warn("NativeLogger", "Failed to delete log file")
102
92
  }
103
93
  }
@@ -23,10 +23,48 @@ object OneKeyLog {
23
23
  private const val MAX_FILE_SIZE = 20L * 1024 * 1024 // 20 MB
24
24
  private const val MAX_HISTORY = 6
25
25
  private const val TOTAL_SIZE_CAP = MAX_FILE_SIZE * MAX_HISTORY
26
+ private const val DEBUG_INFO_RATE_PER_SECOND = 400.0
27
+ private const val DEBUG_INFO_BURST = 2000.0
28
+ private const val WARN_RATE_PER_SECOND = 1000.0
29
+ private const val WARN_BURST = 2000.0
26
30
 
27
31
  // Cached value; empty string means context was not yet available (will retry)
28
32
  @Volatile
29
33
  private var cachedLogsDir: String? = null
34
+ private data class TokenBucket(
35
+ val ratePerSecond: Double,
36
+ val burstCapacity: Double,
37
+ var tokens: Double,
38
+ var lastRefillAtMs: Long,
39
+ ) {
40
+ fun allow(nowMs: Long): Boolean {
41
+ val elapsedMs = (nowMs - lastRefillAtMs).coerceAtLeast(0L)
42
+ if (elapsedMs > 0L) {
43
+ val refill = (elapsedMs.toDouble() / 1000.0) * ratePerSecond
44
+ tokens = minOf(burstCapacity, tokens + refill)
45
+ lastRefillAtMs = nowMs
46
+ }
47
+ if (tokens < 1.0) return false
48
+ tokens -= 1.0
49
+ return true
50
+ }
51
+ }
52
+
53
+ private val rateLimitBuckets: MutableMap<String, TokenBucket> = mutableMapOf(
54
+ "DEBUG" to TokenBucket(
55
+ DEBUG_INFO_RATE_PER_SECOND, DEBUG_INFO_BURST, DEBUG_INFO_BURST, System.currentTimeMillis()
56
+ ),
57
+ "INFO" to TokenBucket(
58
+ DEBUG_INFO_RATE_PER_SECOND, DEBUG_INFO_BURST, DEBUG_INFO_BURST, System.currentTimeMillis()
59
+ ),
60
+ "WARN" to TokenBucket(
61
+ WARN_RATE_PER_SECOND, WARN_BURST, WARN_BURST, System.currentTimeMillis()
62
+ ),
63
+ )
64
+ private val droppedCounts: MutableMap<String, Int> = mutableMapOf()
65
+ @Volatile
66
+ private var lastDropReportMs = System.currentTimeMillis()
67
+ private val rateLimitLock = Any()
30
68
 
31
69
  /**
32
70
  * Initialise OneKeyLog with an Android Context before NitroModules is ready.
@@ -154,7 +192,61 @@ object OneKeyLog {
154
192
  return truncate("$time | $level : [$safeTag] $safeMessage")
155
193
  }
156
194
 
195
+ private fun buildDropReportLocked(nowMs: Long): String? {
196
+ if (nowMs - lastDropReportMs < 1000L) return null
197
+ if (droppedCounts.isEmpty()) {
198
+ lastDropReportMs = nowMs
199
+ return null
200
+ }
201
+ val ordered = listOf("DEBUG", "INFO", "WARN")
202
+ val parts = ordered.mapNotNull { level ->
203
+ val count = droppedCounts[level] ?: 0
204
+ if (count > 0) "$level=$count" else null
205
+ }
206
+ droppedCounts.clear()
207
+ lastDropReportMs = nowMs
208
+ if (parts.isEmpty()) return null
209
+ return "[OneKeyLog] Rate-limited logs (last 1s): ${parts.joinToString(", ")}"
210
+ }
211
+
212
+ private data class RateLimitDecision(val drop: Boolean, val report: String?)
213
+
214
+ private fun evaluateRateLimit(level: String): RateLimitDecision {
215
+ synchronized(rateLimitLock) {
216
+ val nowMs = System.currentTimeMillis()
217
+ var report = buildDropReportLocked(nowMs)
218
+
219
+ // Never limit error logs.
220
+ if (level == "ERROR") {
221
+ return RateLimitDecision(drop = false, report = report)
222
+ }
223
+
224
+ val bucket = rateLimitBuckets[level]
225
+ if (bucket == null) {
226
+ return RateLimitDecision(drop = false, report = report)
227
+ }
228
+ if (bucket.allow(nowMs)) {
229
+ return RateLimitDecision(drop = false, report = report)
230
+ }
231
+ droppedCounts[level] = (droppedCounts[level] ?: 0) + 1
232
+ report = buildDropReportLocked(nowMs) ?: report
233
+ return RateLimitDecision(drop = true, report = report)
234
+ }
235
+ }
236
+
237
+ private fun emitRateLimitReport(report: String) {
238
+ val l = logger
239
+ if (l != null) {
240
+ l.warn(report)
241
+ } else {
242
+ android.util.Log.w("OneKeyLog", report)
243
+ }
244
+ }
245
+
157
246
  private fun log(tag: String, level: String, message: String, androidLogLevel: Int) {
247
+ val decision = evaluateRateLimit(level)
248
+ decision.report?.let { emitRateLimitReport(it) }
249
+ if (decision.drop) return
158
250
  val formatted = formatMessage(tag, level, message)
159
251
  val l = logger
160
252
  if (l != null) {
@@ -22,24 +22,6 @@ class NativeLogger: HybridNativeLoggerSpec {
22
22
  return patterns.compactMap { try? NSRegularExpression(pattern: $0) }
23
23
  }()
24
24
 
25
- /// Rate limiting: max messages per second
26
- private static let maxMessagesPerSecond = 100
27
- private static var messageCount = 0
28
- private static var windowStart = Date()
29
- private static let rateLimitLock = NSLock()
30
-
31
- private static func isRateLimited() -> Bool {
32
- rateLimitLock.lock()
33
- defer { rateLimitLock.unlock() }
34
- let now = Date()
35
- if now.timeIntervalSince(windowStart) >= 1.0 {
36
- windowStart = now
37
- messageCount = 0
38
- }
39
- messageCount += 1
40
- return messageCount > maxMessagesPerSecond
41
- }
42
-
43
25
  private static func sanitize(_ message: String) -> String {
44
26
  var result = message
45
27
  for regex in sensitivePatterns {
@@ -55,8 +37,24 @@ class NativeLogger: HybridNativeLoggerSpec {
55
37
  return result
56
38
  }
57
39
 
40
+ private static func truncateFile(atPath path: String) throws {
41
+ guard FileManager.default.fileExists(atPath: path) else { return }
42
+ let handle = try FileHandle(forWritingTo: URL(fileURLWithPath: path))
43
+ defer {
44
+ if #available(iOS 13.0, *) {
45
+ try? handle.close()
46
+ } else {
47
+ handle.closeFile()
48
+ }
49
+ }
50
+ if #available(iOS 13.0, *) {
51
+ try handle.truncate(atOffset: 0)
52
+ } else {
53
+ handle.truncateFile(atOffset: 0)
54
+ }
55
+ }
56
+
58
57
  func write(level: Double, msg: String) {
59
- if NativeLogger.isRateLimited() { return }
60
58
  let sanitized = NativeLogger.sanitize(msg)
61
59
  switch Int(level) {
62
60
  case 0: OneKeyLog.debug("JS", sanitized)
@@ -93,6 +91,7 @@ class NativeLogger: HybridNativeLoggerSpec {
93
91
 
94
92
  func deleteLogFiles() throws -> Promise<Void> {
95
93
  return Promise.async {
94
+ DDLog.flushLog()
96
95
  let dir = OneKeyLog.logsDirectory
97
96
  let fm = FileManager.default
98
97
  let files: [String]
@@ -102,10 +101,14 @@ class NativeLogger: HybridNativeLoggerSpec {
102
101
  OneKeyLog.warn("NativeLogger", "Failed to list log directory for deletion")
103
102
  return
104
103
  }
105
- // Skip the active log file to avoid breaking CocoaLumberjack's open file handle
106
- for file in files where file.hasSuffix(".log") && file != "app-latest.log" {
104
+ for file in files where file.hasSuffix(".log") {
105
+ let path = "\(dir)/\(file)"
107
106
  do {
108
- try fm.removeItem(atPath: "\(dir)/\(file)")
107
+ if file == "app-latest.log" {
108
+ try NativeLogger.truncateFile(atPath: path)
109
+ } else {
110
+ try fm.removeItem(atPath: path)
111
+ }
109
112
  } catch {
110
113
  OneKeyLog.warn("NativeLogger", "Failed to delete log file")
111
114
  }
@@ -6,6 +6,7 @@ private class OneKeyLogFileManager: DDLogFileManagerDefault {
6
6
  private static let logPrefix = "app"
7
7
  private static let latestFileName = "\(logPrefix)-latest.log"
8
8
  private static let dateFormatterLock = NSLock()
9
+ private static let archiveLock = NSLock()
9
10
  private static let _dateFormatter: DateFormatter = {
10
11
  let fmt = DateFormatter()
11
12
  fmt.dateFormat = "yyyy-MM-dd"
@@ -30,8 +31,11 @@ private class OneKeyLogFileManager: DDLogFileManagerDefault {
30
31
 
31
32
  /// When rolled, rename app-latest.log → app-{yyyy-MM-dd}.{i}.log (matches Android pattern)
32
33
  override func didArchiveLogFile(atPath logFilePath: String, wasRolled: Bool) {
34
+ Self.archiveLock.lock()
35
+ defer { Self.archiveLock.unlock() }
36
+
33
37
  guard wasRolled else {
34
- super.didArchiveLogFile(atPath: logFilePath, wasRolled: wasRolled)
38
+ runCleanup()
35
39
  return
36
40
  }
37
41
 
@@ -53,16 +57,37 @@ private class OneKeyLogFileManager: DDLogFileManagerDefault {
53
57
  try fm.moveItem(atPath: logFilePath, toPath: archivedPath)
54
58
  moved = true
55
59
  } catch {
56
- // Another thread may have created this file; try next index
57
- index += 1
60
+ // Another callback may have already moved the source file.
61
+ if !fm.fileExists(atPath: logFilePath) {
62
+ break
63
+ }
64
+ let nsError = error as NSError
65
+ // Retry only for recoverable "target exists" collisions.
66
+ if nsError.domain == NSCocoaErrorDomain &&
67
+ nsError.code == NSFileWriteFileExistsError {
68
+ index += 1
69
+ continue
70
+ }
71
+ NSLog(
72
+ "[OneKeyLog] Failed to archive log file move (%@:%ld): %@",
73
+ nsError.domain,
74
+ nsError.code,
75
+ nsError.localizedDescription
76
+ )
77
+ break
58
78
  }
59
79
  }
60
80
  if !moved {
61
81
  NSLog("[OneKeyLog] Failed to archive log file after 1000 attempts: %@", logFilePath)
62
- super.didArchiveLogFile(atPath: logFilePath, wasRolled: wasRolled)
63
- } else {
64
- // Pass the new path so the super class can find the file for cleanup
65
- super.didArchiveLogFile(atPath: archivedPath, wasRolled: wasRolled)
82
+ }
83
+ runCleanup()
84
+ }
85
+
86
+ private func runCleanup() {
87
+ do {
88
+ try cleanupLogFiles()
89
+ } catch {
90
+ NSLog("[OneKeyLog] Failed to cleanup log files: %@", (error as NSError).localizedDescription)
66
91
  }
67
92
  }
68
93
  }
@@ -71,6 +96,56 @@ private class OneKeyLogFileManager: DDLogFileManagerDefault {
71
96
 
72
97
  private static let maxMessageLength = 4096
73
98
 
99
+ private struct TokenBucket {
100
+ let ratePerSecond: Double
101
+ let burstCapacity: Double
102
+ var tokens: Double
103
+ var lastRefillAt: TimeInterval
104
+
105
+ mutating func allow(at now: TimeInterval) -> Bool {
106
+ let elapsed = max(0, now - lastRefillAt)
107
+ if elapsed > 0 {
108
+ tokens = min(burstCapacity, tokens + elapsed * ratePerSecond)
109
+ lastRefillAt = now
110
+ }
111
+ guard tokens >= 1 else { return false }
112
+ tokens -= 1
113
+ return true
114
+ }
115
+ }
116
+
117
+ private static let debugInfoRatePerSecond = 400.0
118
+ private static let debugInfoBurst = 2000.0
119
+ private static let warnRatePerSecond = 1000.0
120
+ private static let warnBurst = 2000.0
121
+
122
+ private static var rateLimitBuckets: [String: TokenBucket] = {
123
+ let now = Date().timeIntervalSinceReferenceDate
124
+ return [
125
+ "DEBUG": TokenBucket(
126
+ ratePerSecond: debugInfoRatePerSecond,
127
+ burstCapacity: debugInfoBurst,
128
+ tokens: debugInfoBurst,
129
+ lastRefillAt: now
130
+ ),
131
+ "INFO": TokenBucket(
132
+ ratePerSecond: debugInfoRatePerSecond,
133
+ burstCapacity: debugInfoBurst,
134
+ tokens: debugInfoBurst,
135
+ lastRefillAt: now
136
+ ),
137
+ "WARN": TokenBucket(
138
+ ratePerSecond: warnRatePerSecond,
139
+ burstCapacity: warnBurst,
140
+ tokens: warnBurst,
141
+ lastRefillAt: now
142
+ ),
143
+ ]
144
+ }()
145
+ private static var droppedCounts: [String: Int] = [:]
146
+ private static var lastDropReportAt = Date().timeIntervalSinceReferenceDate
147
+ private static let rateLimitLock = NSLock()
148
+
74
149
  private static let configured: Bool = {
75
150
  let logsDir = logsDirectory
76
151
 
@@ -115,6 +190,63 @@ private class OneKeyLogFileManager: DDLogFileManagerDefault {
115
190
  return _timeFormatter.string(from: date)
116
191
  }
117
192
 
193
+ private static func rateLimitReportLocked(now: TimeInterval) -> String? {
194
+ guard now - lastDropReportAt >= 1.0 else { return nil }
195
+ guard !droppedCounts.isEmpty else {
196
+ lastDropReportAt = now
197
+ return nil
198
+ }
199
+ let ordered = ["DEBUG", "INFO", "WARN"]
200
+ let parts = ordered.compactMap { level -> String? in
201
+ guard let count = droppedCounts[level], count > 0 else { return nil }
202
+ return "\(level)=\(count)"
203
+ }
204
+ droppedCounts.removeAll()
205
+ lastDropReportAt = now
206
+ guard !parts.isEmpty else { return nil }
207
+ return "[OneKeyLog] Rate-limited logs (last 1s): \(parts.joined(separator: ", "))"
208
+ }
209
+
210
+ private static func evaluateRateLimit(for level: String) -> (drop: Bool, report: String?) {
211
+ rateLimitLock.lock()
212
+ defer { rateLimitLock.unlock() }
213
+
214
+ let now = Date().timeIntervalSinceReferenceDate
215
+ var report = rateLimitReportLocked(now: now)
216
+
217
+ // Never limit error logs.
218
+ if level == "ERROR" {
219
+ return (false, report)
220
+ }
221
+
222
+ guard var bucket = rateLimitBuckets[level] else {
223
+ return (false, report)
224
+ }
225
+ let allowed = bucket.allow(at: now)
226
+ rateLimitBuckets[level] = bucket
227
+ if allowed {
228
+ return (false, report)
229
+ }
230
+ droppedCounts[level, default: 0] += 1
231
+ report = rateLimitReportLocked(now: now) ?? report
232
+ return (true, report)
233
+ }
234
+
235
+ private static func log(
236
+ _ level: String,
237
+ _ tag: String,
238
+ _ message: String,
239
+ writer: (String) -> Void
240
+ ) {
241
+ _ = configured
242
+ let decision = evaluateRateLimit(for: level)
243
+ if let report = decision.report {
244
+ DDLogWarn(report)
245
+ }
246
+ if decision.drop { return }
247
+ writer(formatMessage(tag, level, message))
248
+ }
249
+
118
250
  private static func truncate(_ message: String) -> String {
119
251
  if message.count > maxMessageLength {
120
252
  return String(message.prefix(maxMessageLength)) + "...(truncated)"
@@ -163,23 +295,19 @@ private class OneKeyLogFileManager: DDLogFileManagerDefault {
163
295
  }
164
296
 
165
297
  @objc public static func debug(_ tag: String, _ message: String) {
166
- _ = configured
167
- DDLogDebug(formatMessage(tag, "DEBUG", message))
298
+ log("DEBUG", tag, message) { DDLogDebug($0) }
168
299
  }
169
300
 
170
301
  @objc public static func info(_ tag: String, _ message: String) {
171
- _ = configured
172
- DDLogInfo(formatMessage(tag, "INFO", message))
302
+ log("INFO", tag, message) { DDLogInfo($0) }
173
303
  }
174
304
 
175
305
  @objc public static func warn(_ tag: String, _ message: String) {
176
- _ = configured
177
- DDLogWarn(formatMessage(tag, "WARN", message))
306
+ log("WARN", tag, message) { DDLogWarn($0) }
178
307
  }
179
308
 
180
309
  @objc public static func error(_ tag: String, _ message: String) {
181
- _ = configured
182
- DDLogError(formatMessage(tag, "ERROR", message))
310
+ log("ERROR", tag, message) { DDLogError($0) }
183
311
  }
184
312
 
185
313
  /// Returns the logs directory path (for getLogFilePaths / deleteLogFiles)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-native-logger",
3
- "version": "1.1.27",
3
+ "version": "1.1.29",
4
4
  "description": "react-native-native-logger",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",