Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

19 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

primitives-kt

A batteries-included Kotlin primitives library — the Kotlin/JVM (and, where it makes sense, Android) port of primitives-go: observability, HTTP, resilience, caching, crypto, analytics, feature flags, push and a couple dozen more — each behind a small, consistent interface, traced and log-correlated end to end.

Scope: the client. Kotlin builds apps here, never services — platform-go is the only server tier there is — so this library carries nothing whose reason to exist is sitting beside a database, a broker, an object store or a secret manager. Talking to a service built on platform-go is platform-client-kt's job, not this library's.

Identifiers are the case worth naming, because the package name does not sound server-tier: a server issues IDs and a client receives opaque strings, so minting one here only risks having it rejected by the service you send it to. There is no :identifiers module.

Status: scoped to the client tier — 44 Gradle modules, building, testing and linting green. Parity with primitives-go is deliberately partial; see below. Pure-JVM modules are consumable from any server/JVM project with no Android tooling (see Using primitives-kt).

Jump to: What it gives you · Quick taste · Module catalog · Using it · Building · Guides: cookbook · observability · porting status

What it gives you

Every package follows the same anatomy, so once you've learned one you've learned them all:

  • An -api module — the interface you program against, a config DSL that validate()s at construction (so misconfiguration fails loudly at startup, not silently at first use), and noop + mock doubles for tests.
  • One or more backend modules — the real implementation over a vendor SDK (Redis, Postgres, S3, Stripe, Anthropic, …). Additional vendors are documented TODO(<vendor>) seams, not silent gaps.

Three properties run through the whole tree:

  1. Traced & correlated. A value you record on an operation lands on both its trace span and a span-linked logger, and downstream HTTP/DB/queue calls parent under it automatically — via the coroutine context, with no ctx parameter to thread. Observability is the founding pillar everything else plugs into.
  2. Resilient by construction. Every remote call accepts an optional circuit breaker; retries, backoff, and rate limiting are first-class and coroutine-native.
  3. Noops everywhere. Telemetry, breakers, and backends all degrade to no-ops when absent — nothing throws for want of a logger. You wire in exactly what you need.

Dual-target. Kotlin runs on the JVM and Android, so the pure-JVM modules serve desktop and tooling consumers while a handful of packages add an Android counterpart. Note that several of these are the receiving half of a name that means the opposite in primitives-go: eventstream consumes SSE/WS into a Flow, notifications receives push rather than sending it, and uploads is the signed-URL client uploader, not an object-store sink.

Quick taste

The founding pillar — one Observer per component, a span { } per traced call, and a set that writes to the span and the correlated log line at once:

class UserRepository(observers: ObserverFactory, private val api: UserApi) {
    private val o11y = observers.named("user_repository")

    suspend fun load(userId: String): User = o11y.span("load") {
        set(Keys.USER_ID to userId)          // lands on the span AND the span-linked logger
        logger.debug("loading user")
        api.fetch(userId)                     // if api is instrumented, its span nests under "load"
    }                                         // span ends on return; any throw is recorded + logged
}

A few more packages, to show the house style (full cookbook in docs/EXAMPLES.md):

// Resilient, traced HTTP — OkHttp (client/Android) or Ktor (server) behind one interface
val http: HttpClient = OkHttpHttpClient.create(HttpClientConfig { timeout = 5.seconds })
val resp = http.execute(HttpRequest.get("https://api.example.com/health"))

// A circuit breaker around a flaky dependency
val breaker = CircuitBreaker { failureThreshold = 5; resetTimeout = 10.seconds }
val data = breaker.execute { dependency.call() }        // throws ErrCircuitBroken while OPEN

// A typed cache — swap InMemoryCache for the DataStore-backed one, same interface
val cache: BatchCache<String> = InMemoryCache()
cache.set("greeting", "hello"); cache.get("greeting")   // "hello"

// A token bucket, so you throttle before you spend the request
val limiter = InMemoryRateLimiter(requestsPerSecond = 10.0, burst = 20)
if (limiter.allow("sync")) syncNow()

Module catalog

44 modules, grouped by the tier they were ported in. Depend on just the ones you need — every JVM module depends only on other JVM modules. Coordinates are com.github.primandproper.primitives-kt:<module>:<tag> (see Using it).

Observability — the founding pillar

Module Type What
:observability-api JVM The contract: Observer, Operation, Logger, Tracer, span { }, Keys, config DSL, noop backends.
:observability-otel JVM OTel SDK + OTLP exporter + head sampling + W3C propagation.
:observability-logcat Android LogcatLogger over android.util.Log.
:observability-koin JVM Optional Koin module for DI.
:observability-testing JVM RecordingObserver test double + framework-neutral matchers.
:observability Android Umbrella Observability { } builder that wires the backends.

Full walkthrough: docs/USAGE.md.

Tier 1 — foundation & networking spine

The small, widely-depended-on substrate. All pure Kotlin/JVM, coroutine-native, traced.

Module What
:errors Sentinels + wrap/isError/asError; HTTP ErrorCode→toApiError/toHttpStatus; gRPC GrpcCode mapping. Owns ErrCircuitBroken.
:random Crypto-secure random strings/bytes over SecureRandom (hex/Base32/Base64url/custom alphabet).
:retry Coroutine-native exponential backoff + jitter: Policy.execute { } and a Flow.retryWithPolicy variant.
:circuitbreaking execute { } breaker with a CircuitState StateFlow, plus a partitioned per-key registry.
:httpclient-api The HTTP contract: request/response models, config DSL, retry hook, header redaction, OTel span recording.
:httpclient-okhttp / :httpclient-ktor OkHttp (client/Android) and Ktor/CIO (server) backends behind that one contract.

Tier 2 — core cross-surface services

Each ships core -api + one primary backend + a real Android module.

Module(s) What
:cache-api · :cache-android Typed Cache/BatchCache: in-memory + DataStore on Android.
:cryptography-api · :cryptography-jvm · :cryptography-android AES-256-GCM AEAD + SHA/checksum hashers; AndroidKeyStore backend.
:secrets-api · :secrets-android SecretSource: env provider (value never touches a span/log) + EncryptedSharedPreferences.
:featureflags-api · :featureflags-launchdarkly · :featureflags-android Flag evaluation: in-memory + LaunchDarkly (breaker-wrapped) + local store.
:analytics-api · :analytics-segment · :analytics-android Event reporting: Segment + multisource composite + on-device buffering.

Tier 3 — transport & storage

What survived the server-tier cut: the pieces a client still needs on its own side of the wire.

Module(s) What
:ratelimiting-api Token-bucket RateLimiter, in-memory — client-side throttling before you spend a request.
:uploads-api · :uploads-android UploadManager/Bucket: in-mem/filesystem + signed-URL client uploader.
:eventstream-api · :eventstream-android SSE/WS consumed into a Flow on Android.
:cookies · :encoding AES-GCM sealed cookies · kotlinx.serialization JSON.

Tier 4 — domain, AI & utilities

Module(s) What
:notifications-api · :notifications-fcm · :notifications-android Push via FCM HTTP v1 (send) + Android receive-side mapping.
:qrcodes ZXing PNG QR codes for otpauth:// URIs (pairs with TOTP).
:compression · :files · :numbers · :bitmask zstd/S2 · nio line/chunk readers · BigDecimal helpers · width-typed bitmasks.
:version · :fake · :testutils Build/VCS metadata · seedable data generator · Testcontainers helpers.

The authoritative per-package status — which backends shipped and which vendor seams remain open — is docs/PORTING_STATUS.md.

Using primitives-kt in your project

primitives-kt publishes through JitPack — no artifact registry to log into and no credentials. To cut a release, make the GitHub repo public and push a tag:

git tag v0.1.0 && git push origin v0.1.0

Then, in the consuming project, add the JitPack repository and depend on the modules you want by coordinate — com.github.primandproper.primitives-kt:<module>:<tag>:

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        mavenCentral()
        maven("https://jitpack.io")
    }
}

// build.gradle.kts
dependencies {
    implementation("com.github.primandproper.primitives-kt:cache-api:v0.1.0")
}

JitPack builds the tag on first request (later resolves are cached and fast). It compiles only the pure-JVM modules — the Android-library modules are excluded via -PjvmOnly (see jitpack.yml and the toggle in settings.gradle.kts) — so a JVM project consumes primitives-kt with no Android tooling on either side, and JitPack itself never needs the Android SDK. Every JVM module depends only on other JVM modules, so no .aar is ever pulled into a server build.

Android modules (.aar)

Ten packages ship a real Android-library counterpart. They carry the same group and version scheme as the JVM modules — com.github.primandproper.primitives-kt:<module>:<tag> — but package as .aar:

Module (artifact ID) What
observability-logcat LogcatLogger over android.util.Log.
observability Umbrella Observability { } builder that wires the backends.
secrets-android SecretSource over EncryptedSharedPreferences.
analytics-android On-device event buffering.
featureflags-android Local flag store.
cryptography-android AES-256-GCM AEAD over the AndroidKeyStore.
cache-android Disk-backed BatchCache on Jetpack DataStore.
uploads-android Signed-URL client uploader.
eventstream-android Consumes SSE/WS into a Flow.
notifications-android FCM receive-side mapping.

Android consumer requirements

These .aars compile to Java 17 bytecode (sourceCompatibility/targetCompatibility = 17), so consuming them requires AGP 8.x (which understands Java-17 class files) and a project that itself compiles against Java 17.

Core-library desugaring is required on minSdk < 26. Every Android module transitively exposes opentelemetry-api (via :observability-api), which references java.time / java.util.function — APIs only present natively from API 26. All ten modules ship with isCoreLibraryDesugaringEnabled = true, but desugaring is a consumer-side transform: your app module must also enable it, or java.time usage will crash at runtime on pre-26 devices.

android {
    compileOptions {
        isCoreLibraryDesugaringEnabled = true
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}
dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.3")
}

Per-module minSdk (the shared floor is 21; two modules raise it for the AndroidKeyStore):

Module minSdk Desugaring needed (minSdk < 26)
observability 21 Yes
observability-logcat 21 Yes
analytics-android 21 Yes
cache-android 21 Yes
eventstream-android 21 Yes
featureflags-android 21 Yes
uploads-android 21 Yes
notifications-android 21 Yes
secrets-android 23 Yes
cryptography-android 23 Yes

secrets-android and cryptography-android require API 23 because EncryptedSharedPreferences / MasterKey and the AndroidKeyStore AES-256-GCM key scheme are only available from API 23.

Remote channel — GitHub Packages

JitPack serves the JVM modules (its build runs -PjvmOnly, so it never needs the Android SDK). The ten Android .aar modules are published separately to this repo's GitHub Packages Maven registry by a tag-triggered CI job (.github/workflows/publish.yml): pushing a v* tag runs the full (non-jvmOnly) publishAllPublicationsToGitHubPackagesRepository at the tag's version, so a failure there can never regress JitPack's reliable JVM flow.

Consume them by adding the authenticated GitHub Packages repository (GitHub requires a token even for public packages) and depending on the same coordinate as the JVM modules:

repositories {
    maven {
        url = uri("https://maven.pkg.github.com/primandproper/primitives-kt")
        credentials {
            username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
            password = providers.gradleProperty("gpr.token").orNull ?: System.getenv("GITHUB_TOKEN")
        }
    }
}
dependencies {
    implementation("com.github.primandproper.primitives-kt:cryptography-android:v0.1.0")
}

For pre-tag experimentation, a local checkout + ./gradlew publishToMavenLocal (which builds the .aars — see Local iteration) consumed via mavenLocal() still works and needs no credentials.

Local iteration without a tag

To try changes before tagging, publish to your Maven Local repo and consume via mavenLocal():

./gradlew publishToMavenLocal            # every module, incl. Android .aar (needs the Android SDK)
./gradlew publishToMavenLocal -PjvmOnly  # JVM modules only, no Android SDK required

The coordinate is the same com.github.primandproper.primitives-kt:<module> either way, so your implementation(...) lines are identical for JitPack and Maven Local — only the repository differs.

Building

Requires JDK 17 and the Android SDK (local.properties with sdk.dir, or ANDROID_HOME). Easiest path is to open the project in Android Studio. The Gradle wrapper is committed, so ./gradlew works out of the box — from the CLI just use the Makefile:

make build     # compile + assemble
make fmt       # ktlintFormat
make lint      # ktlintCheck
make test      # JVM unit tests
make check     # lint + format + tests — same command CI runs

Run make help for the full target list. Every PR is gated by .github/workflows/ci.yml, which runs make check (ktlint + unit tests + Android Lint) — the same target you run locally, so a green make check predicts a green CI run.

Design notes

  • Context propagation is implicit. span { } installs the span into the coroutine context via OTel's Context.asContextElement(), so nested suspend calls parent correctly across dispatcher threads. spanBlocking { } is the thread-local variant for non-coroutine call sites (WorkManager, callbacks, Java interop).
  • Explicit operation names. There's no cheap analog of Go's runtime.Callers caller-naming under Kotlin inlining, so span("name") takes the name. Terse, and zero reflection on the hot path.
  • Noops everywhere. A nil logger or tracer degrades to a noop; a non-recording span makes every set/error a no-op. Nothing panics for want of observability.
  • OTel Span is aliased, not re-abstracted (typealias Span = io.opentelemetry.api.trace.Span), exactly as platform-go aliases trace.Span.
  • Faithful to the source. Ports are validated by comparison against ../platform-go, not by behavioural equivalence testing against the Go binaries. Deliberate divergences (coroutine-context propagation, AES-GCM cookie sealing vs gorilla's wire format, Snappy-framed S2) are documented where they occur.

Deliberately descoped (for now)

  • Metrics & profiling pillars — named, not silently dropped. When metrics lands, Observability/ObservabilityConfig/provider enums extend back toward Go's 4-pillar shape, and the TODO(metrics) seams across circuitbreaking/cache/ratelimiting/… reattach.
  • Three Go idioms with no worthwhile Kotlin analog — pointer (Kotlin nullability obviates it), reflection, and panicking (exceptions + runCatching cover it).

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages