Skip to main content

Logs Collector

KapeLogsCollector sends this device's logs to the Kape Logs Collector Service so support can read them. It can also write them to a zip file for the user to share themselves.

An upload opens a report and attaches log files to it. The report has a short code: show it to the user, or pass it along with a support ticket, so support can find the logs.

Uploads are anonymous: the user does not need to be signed in. The SDK adds the OS, OS version, app version, SDK version and device model to every report.

What gets uploaded​

SourceKapeLogSourceFile on the report
Low Level SDK logsSDKkp-<n>.jsonl, one per log file
Platform SDK log, including the VPN service'sPLATFORMplatform-app.jsonl
Your own files—the name you give each KapeLogAttachment

KapeLogSources.ALL, the default, is both sources. A source with nothing logged is skipped.

KapeVpnService runs in your app's process, so its log lines are in platform-app.jsonl along with the rest of the Platform SDK's. Unlike iOS, where the tunnel is a separate process, there is no platform-tunnel.jsonl.

The platform log also carries the Low Level SDK's log lines, tagged with the process llsdk. In every line, the Platform SDK's own and the Low Level SDK's, IPv4 and IPv6 addresses and hostnames on common top-level domains are replaced with <redacted>. The redaction matches patterns, so treat the platform log as possibly sensitive too. The SDK source is not redacted.

Setup​

1. Turn on file logging​

In res/xml/kape_sdk_settings.xml:

<kape-sdk-settings
...
max-log-level="Debug"
file-logging-enabled="true" />

max-log-level must be Info, Debug or Trace.

danger

At Warn or below the Low Level SDK writes no log files, and deletes the ones already there. The SDK logs a warning at start-up when file-logging-enabled is set with such a level.

With file logging on, the SDK writes:

  • the Low Level SDK's encrypted log files, under noBackupFilesDir/KapeSDK/logs. The encryption key is kept in the SDK's own storage, so only your app can read them;
  • the Platform SDK's own log, as JSON Lines, to noBackupFilesDir/kape_sdk_log-app.jsonl. It rotates at 256 KB, keeping one older file. Logcat output does not change, and Low Level SDK lines in Logcat are not redacted.

Nothing under noBackupFilesDir is included in Android backups.

Optionally, set limits on the Low Level SDK's log files:

<kape-sdk-settings
...
file-logging-enabled="true"
prelog-max-mb="2"
prelog-max-minutes="60"
log-file-max-mb="50"
log-retention-hours="168" />
AttributeDefaultMeaning
prelog-max-mb4Size of the in-memory buffer kept while the level is below Info
prelog-max-minutes60Age of the oldest record that buffer keeps
log-file-max-mb100Size at which a log file rotates
log-retention-hours168Age after which log files are deleted

Each value must be a non-negative whole number; anything else fails KapeSDKSettings.fromXml.

2. Point at your Logs Collector tenant​

Set logs_collector_api_domain under domains: in your YAML config. Give the staging value in your staging YAML: the Low Level SDK does not read environment, so a domain you leave out defaults to production.

domains:
# Production
logs_collector_api_domain: "https://cp.expressapisv2.net/apigw/lcs/lcs/tenants/xv/"
# Staging
# logs_collector_api_domain: "https://xv-cp.apis-staging.xvtest.net/apigw/lcs/lcs/tenants/xv/"

These are the ExpressVPN (xv) tenant URLs. For another brand, replace /tenants/xv/ with your tenant.

Uploading​

For a working example, see VpnDemo's Settings → SDK Logs screen, whose menu uploads (showing the report code) or exports a zip to the share sheet: android/Examples/VpnDemo/src/main/java/com/kape/vpndemo/logs/LogViewerViewModel.kt.

uploadLogs does everything in one call. It flushes the logs to disk, opens a report, uploads the chosen sources, then attaches your files:

import com.kape.platformsdk.core.KapeError
import com.kape.platformsdk.core.logging.KapeLogAttachment
import com.kape.platformsdk.core.logging.KapeLogSources
import com.kape.platformsdk.core.logging.logsCollector

try {
val result = handle.logsCollector().uploadLogs(
sources = KapeLogSources.ALL,
attributes = mapOf("support_channel" to "contact_form"),
attachments = listOf(
KapeLogAttachment("report.txt", diagnostics.toByteArray()),
KapeLogAttachment("app.log", appLogBytes),
),
)
showToUser(result.code)
result.failed.forEach { Log.w(TAG, "Not uploaded: ${it.filename} — ${it.error.message}") }
} catch (e: KapeError) {
// The report could not be opened, or not a single file was attached.
Log.e(TAG, "${e.domain} ${e.httpStatus}: ${e.message}")
}

When one file fails, the others are still uploaded, and the failure is listed in result.failed. The call throws only when an attribute or file name is invalid, when the report cannot be opened, or when no file at all was attached. The Low Level SDK logs succeed or fail as a batch, listed as KapeLogUploadFailure.SDK_LOGS.

KapeError.httpStatus and KapeError.httpBody give the service's response when it rejected a request.

Progress and cancelling​

val result = collector.uploadLogs { completed, total ->
progress.update { completed.toFloat() / maxOf(total, 1) }
!userCancelled // return false to stop
}
if (result.isCancelled) { /* files already uploaded stay on the report */ }

The callback can run on a background thread. The total grows by the number of Low Level SDK files once their upload starts. Cancelling the calling coroutine also stops the upload.

Step by step​

To keep the report open, for example to add a ticket ID once your helpdesk returns one, use createReport:

val report = collector.createReport()
val result = report.uploadLogs(sources = setOf(KapeLogSource.SDK, KapeLogSource.PLATFORM))
report.attach(KapeLogAttachment("report.txt", reportBytes))

val ticket = helpdesk.submit(message, logsCode = report.code)
report.setAttributes(mapOf("zendesk_ticket" to ticket.id))

report.uploadLogs does not throw a KapeError: every failure is in result.failed. Unlike collector.uploadLogs, it does not flush first — call collector.flush() beforehand if the last records matter.

Attributes​

Attributes are key/value pairs that support staff see on the report. They are checked before any request is made, and a broken rule throws a KapeError with domain "Logging":

  • at most 20 per report;
  • keys are 1–64 characters of a-z, 0-9, _, ., -, starting with a letter;
  • values are 1–128 bytes, with no control characters;
  • os_name, os_version, app_version, sdk_version, device_model, and keys starting kp_ or kp., are reserved.

Each key can be set once. Setting it again to the same value does nothing; setting it to another value fails with HTTP 409.

warning

Support staff see attribute values. Never put personal data, such as an email address, in them.

Exporting to a file​

exportLogs writes the same logs to a zip file, without any network request:

val zip = File(context.cacheDir, "logs/logs.zip")
val count = collector.exportLogs(
zipFile = zip,
attachments = listOf(KapeLogAttachment("report.txt", reportBytes)),
)
// Share `zip`, e.g. through a FileProvider and an ACTION_SEND intent.

It replaces any file already at zipFile, and returns the number of files in the archive. The Low Level SDK logs are decrypted, and not redacted, in the archive, so treat it as sensitive. The archive is built beside zipFile under a temporary name and renamed into place once complete, so a failed export leaves any existing file untouched and no partial file behind. With no logs to export, the archive is written empty.

Attachment file names must not contain a path, and must not repeat each other or a platform log file name.

Flushing​

uploadLogs and exportLogs flush first. KapeVpnService also flushes when it is destroyed or its VPN permission is revoked. To flush yourself, for example before your process may be killed, call the blocking flush() off the main thread:

withContext(Dispatchers.IO) { handle.logsCollector().flush() }