diff --git a/.github/workflows/android_build.yml b/.github/workflows/android_build.yml index 033ed4ba94..e0e7028bce 100644 --- a/.github/workflows/android_build.yml +++ b/.github/workflows/android_build.yml @@ -100,7 +100,7 @@ jobs: java-package: jdk architecture: x64 - name: 'Install dependencies' - run: ./ci/mac_ci_setup.sh --android + run: ./ci/mac_ci_setup.sh - name: 'Download aar' uses: actions/cache@v2 id: check-cache @@ -128,3 +128,89 @@ jobs: adb shell am start -n io.envoyproxy.envoymobile.helloenvoykotlin/.MainActivity - name: 'Check connectivity' run: adb logcat -e "received headers with status 200" -m 1 + kotlinbaselineapp: + name: kotlin_baseline_app + needs: androidbuild + runs-on: macos-11 + timeout-minutes: 25 + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - uses: actions/setup-java@v1 + with: + java-version: '8' + java-package: jdk + architecture: x64 + - name: 'Install dependencies' + run: ./ci/mac_ci_setup.sh --android + - name: 'Download aar' + uses: actions/cache@v2 + id: check-cache + with: + key: aar-${{ github.sha }} + path: dist/envoy.aar + - name: 'Short-circuit' + if: steps.check-cache.outputs.cache-hit != 'true' + run: exit 1 + - name: 'Start simulator' + run: ./ci/mac_start_emulator.sh + # Return to using: + # ./bazelw mobile-install --fat_apk_cpu=x86 --start_app //examples/kotlin/hello_world:hello_envoy_kt + # When https://github.com/envoyproxy/envoy-mobile/issues/853 is fixed. + - name: 'Start kotlin app' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ./bazelw build \ + --config=remote-ci-macos \ + --remote_header="Authorization=Bearer $GITHUB_TOKEN" \ + --fat_apk_cpu=x86 \ + //test/kotlin/apps/baseline:hello_envoy_kt + adb install -r --no-incremental bazel-bin/test/kotlin/apps/baseline/hello_envoy_kt.apk + adb shell am start -n io.envoyproxy.envoymobile.helloenvoykotlin/.MainActivity + - name: 'Check connectivity' + run: adb logcat -e "received headers with status 200" -m 1 + kotlinexperimentalapp: + name: kotlin_experimental_app + needs: androidbuild + runs-on: macos-11 + timeout-minutes: 25 + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - uses: actions/setup-java@v1 + with: + java-version: '8' + java-package: jdk + architecture: x64 + - name: 'Install dependencies' + run: ./ci/mac_ci_setup.sh + - name: 'Download aar' + uses: actions/cache@v2 + id: check-cache + with: + key: aar-${{ github.sha }} + path: dist/envoy.aar + - name: 'Short-circuit' + if: steps.check-cache.outputs.cache-hit != 'true' + run: exit 1 + - name: 'Start simulator' + run: ./ci/mac_start_emulator.sh + # Return to using: + # ./bazelw mobile-install --fat_apk_cpu=x86 --start_app //examples/kotlin/hello_world:hello_envoy_kt + # When https://github.com/envoyproxy/envoy-mobile/issues/853 is fixed. + - name: 'Start kotlin app' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ./bazelw build \ + --config=remote-ci-macos \ + --remote_header="Authorization=Bearer $GITHUB_TOKEN" \ + --fat_apk_cpu=x86 \ + //test/kotlin/apps/experimental:hello_envoy_kt + adb install -r --no-incremental bazel-bin/test/kotlin/apps/experimental/hello_envoy_kt.apk + adb shell am start -n io.envoyproxy.envoymobile.helloenvoykotlin/.MainActivity + - name: 'Check connectivity' + run: adb logcat -e "received headers with status 200" -m 1 diff --git a/.github/workflows/ios_build.yml b/.github/workflows/ios_build.yml index 5ec62bb7d4..c52f81c717 100644 --- a/.github/workflows/ios_build.yml +++ b/.github/workflows/ios_build.yml @@ -68,6 +68,74 @@ jobs: - run: cat /tmp/envoy.log if: ${{ failure() || cancelled() }} name: 'Log app run' + swiftbaselineapp: + name: swift_baseline_app + needs: iosbuild + runs-on: macos-11 + timeout-minutes: 25 + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - run: ./ci/mac_ci_setup.sh + name: 'Install dependencies' + - uses: actions/cache@v2 + id: check-cache + with: + key: framework-${{ github.sha }} + path: dist/Envoy.framework + name: 'Download framework' + - run: exit 1 + if: steps.check-cache.outputs.cache-hit != 'true' + name: 'Short-circuit' + - env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./bazelw build --config=ios --config=remote-ci-macos --remote_header="Authorization=Bearer $GITHUB_TOKEN" //test/swift/apps/baseline:app + name: 'Build swift app' + # Run the app in the background and redirect logs. + - env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./bazelw run --config=ios --config=remote-ci-macos --remote_header="Authorization=Bearer $GITHUB_TOKEN" //test/swift/apps/baseline:app &> /tmp/envoy.log & + name: 'Run swift app' + - run: sed '/received headers with status 200/q' <(touch /tmp/envoy.log && tail -F /tmp/envoy.log) + name: 'Check connectivity' + - run: cat /tmp/envoy.log + if: ${{ failure() || cancelled() }} + name: 'Log app run' + swiftexperimentalapp: + name: swift_experimental_app + needs: iosbuild + runs-on: macos-11 + timeout-minutes: 25 + steps: + - uses: actions/checkout@v1 + with: + submodules: true + - run: ./ci/mac_ci_setup.sh + name: 'Install dependencies' + - uses: actions/cache@v2 + id: check-cache + with: + key: framework-${{ github.sha }} + path: dist/Envoy.framework + name: 'Download framework' + - run: exit 1 + if: steps.check-cache.outputs.cache-hit != 'true' + name: 'Short-circuit' + - env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./bazelw build --config=ios --config=remote-ci-macos --remote_header="Authorization=Bearer $GITHUB_TOKEN" //test/swift/apps/experimental:app + name: 'Build swift app' + # Run the app in the background and redirect logs. + - env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./bazelw run --config=ios --config=remote-ci-macos --remote_header="Authorization=Bearer $GITHUB_TOKEN" //test/swift/apps/experimental:app &> /tmp/envoy.log & + name: 'Run swift app' + - run: sed '/received headers with status 200/q' <(touch /tmp/envoy.log && tail -F /tmp/envoy.log) + name: 'Check connectivity' + - run: cat /tmp/envoy.log + if: ${{ failure() || cancelled() }} + name: 'Log app run' swiftasyncawait: name: swift_async_await needs: iosbuild diff --git a/test/kotlin/apps/baseline/.bazelproject b/test/kotlin/apps/baseline/.bazelproject new file mode 100644 index 0000000000..651096b4a0 --- /dev/null +++ b/test/kotlin/apps/baseline/.bazelproject @@ -0,0 +1,22 @@ +workspace_type: android + +directories: + -bazel-bin + -bazel-instant-android + -bazel-out + -bazel-testlogs + -buck-out + -build + examples/kotlin/hello_world + +import_run_configurations: + examples/kotlin/hello_world/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml + +targets: + //examples/kotlin/hello_world:hello_envoy_kt + +additional_languages: + kotlin + java + android + c diff --git a/test/kotlin/apps/baseline/AndroidManifest.xml b/test/kotlin/apps/baseline/AndroidManifest.xml new file mode 100644 index 0000000000..89427cc582 --- /dev/null +++ b/test/kotlin/apps/baseline/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/test/kotlin/apps/baseline/AsyncDemoFilter.kt b/test/kotlin/apps/baseline/AsyncDemoFilter.kt new file mode 100644 index 0000000000..a45ec5f7af --- /dev/null +++ b/test/kotlin/apps/baseline/AsyncDemoFilter.kt @@ -0,0 +1,95 @@ +package io.envoyproxy.envoymobile.helloenvoykotlin + +import io.envoyproxy.envoymobile.AsyncResponseFilter +import io.envoyproxy.envoymobile.EnvoyError +import io.envoyproxy.envoymobile.FilterDataStatus +import io.envoyproxy.envoymobile.FilterHeadersStatus +import io.envoyproxy.envoymobile.FilterResumeStatus +import io.envoyproxy.envoymobile.FilterTrailersStatus +import io.envoyproxy.envoymobile.FinalStreamIntel +import io.envoyproxy.envoymobile.ResponseFilterCallbacks +import io.envoyproxy.envoymobile.ResponseHeaders +import io.envoyproxy.envoymobile.ResponseTrailers +import io.envoyproxy.envoymobile.StreamIntel +import java.nio.ByteBuffer +import java.util.Timer +import kotlin.concurrent.schedule + +/** + * Example of a more complex HTTP filter that pauses processing on the response filter chain, + * buffers until the response is complete, then asynchronously triggers filter chain resumption + * while setting a new header. Also demonstrates safety of re-entrancy in async callbacks. + */ +class AsyncDemoFilter : AsyncResponseFilter { + private lateinit var callbacks: ResponseFilterCallbacks + + override fun onResponseHeaders( + headers: ResponseHeaders, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterHeadersStatus { + // If this is the end of the stream, asynchronously resume response processing via callback. + if (endStream) { + Timer("AsyncResume", false).schedule(100) { + callbacks.resumeResponse() + } + } + return FilterHeadersStatus.StopIteration() + } + + override fun onResponseData( + body: ByteBuffer, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterDataStatus { + // If this is the end of the stream, asynchronously resume response processing via callback. + if (endStream) { + Timer("AsyncResume", false).schedule(100) { + callbacks.resumeResponse() + } + } + return FilterDataStatus.StopIterationAndBuffer() + } + + override fun onResponseTrailers( + trailers: ResponseTrailers, + streamIntel: StreamIntel + ): FilterTrailersStatus { + // Trailers imply end of stream, so asynchronously resume response processing via callbacka + Timer("AsyncResume", false).schedule(100) { + callbacks.resumeResponse() + } + return FilterTrailersStatus.StopIteration() + } + + override fun setResponseFilterCallbacks(callbacks: ResponseFilterCallbacks) { + this.callbacks = callbacks + } + + override fun onResumeResponse( + headers: ResponseHeaders?, + data: ByteBuffer?, + trailers: ResponseTrailers?, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterResumeStatus { + val builder = headers!!.toResponseHeadersBuilder() + .add("async-filter-demo", "1") + return FilterResumeStatus.ResumeIteration(builder.build(), data, trailers) + } + + @Suppress("EmptyFunctionBlock") + override fun onError( + error: EnvoyError, + finalStreamIntel: FinalStreamIntel + ) { + } + + @Suppress("EmptyFunctionBlock") + override fun onCancel(finalStreamIntel: FinalStreamIntel) { + } + + @Suppress("EmptyFunctionBlock") + override fun onComplete(finalStreamIntel: FinalStreamIntel) { + } +} diff --git a/test/kotlin/apps/baseline/BUILD b/test/kotlin/apps/baseline/BUILD new file mode 100644 index 0000000000..7ef9cb46c2 --- /dev/null +++ b/test/kotlin/apps/baseline/BUILD @@ -0,0 +1,45 @@ +load("@build_bazel_rules_android//android:rules.bzl", "android_binary") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") +load("@rules_detekt//detekt:defs.bzl", "detekt") +load("@rules_jvm_external//:defs.bzl", "artifact") + +licenses(["notice"]) # Apache 2 + +android_binary( + name = "hello_envoy_kt", + custom_package = "io.envoyproxy.envoymobile.helloenvoykotlin", + manifest = "AndroidManifest.xml", + proguard_specs = ["//library:proguard_rules"], + deps = [ + "hello_envoy_kt_lib", + ], +) + +kt_android_library( + name = "hello_envoy_kt_lib", + srcs = [ + "AsyncDemoFilter.kt", + "BufferDemoFilter.kt", + "DemoFilter.kt", + "MainActivity.kt", + ], + custom_package = "io.envoyproxy.envoymobile.helloenvoykotlin", + manifest = "AndroidManifest.xml", + resource_files = [ + "res/layout/activity_main.xml", + ], + deps = [ + "//dist:envoy_mobile_android", + "//examples/kotlin/shared:hello_envoy_shared_lib", + artifact("androidx.recyclerview:recyclerview"), + artifact("androidx.annotation:annotation"), + artifact("com.google.code.findbugs:jsr305"), + ], +) + +detekt( + name = "hello_envoy_kt_lint", + srcs = glob(["*.kt"]), + build_upon_default_config = True, + cfgs = ["//:kotlin_lint_config"], +) diff --git a/test/kotlin/apps/baseline/BufferDemoFilter.kt b/test/kotlin/apps/baseline/BufferDemoFilter.kt new file mode 100644 index 0000000000..f158d1b1e6 --- /dev/null +++ b/test/kotlin/apps/baseline/BufferDemoFilter.kt @@ -0,0 +1,73 @@ +package io.envoyproxy.envoymobile.helloenvoykotlin + +import io.envoyproxy.envoymobile.EnvoyError +import io.envoyproxy.envoymobile.FilterDataStatus +import io.envoyproxy.envoymobile.FilterHeadersStatus +import io.envoyproxy.envoymobile.FilterTrailersStatus +import io.envoyproxy.envoymobile.FinalStreamIntel +import io.envoyproxy.envoymobile.ResponseFilter +import io.envoyproxy.envoymobile.ResponseHeaders +import io.envoyproxy.envoymobile.ResponseTrailers +import io.envoyproxy.envoymobile.StreamIntel +import java.nio.ByteBuffer + +/** + * Example of a more complex HTTP filter that pauses processing on the response filter chain, + * buffers until the response is complete, then resumes filter iteration while setting a new + * header. + */ +class BufferDemoFilter : ResponseFilter { + private lateinit var headers: ResponseHeaders + private lateinit var body: ByteBuffer + + override fun onResponseHeaders( + headers: ResponseHeaders, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterHeadersStatus { + this.headers = headers + return FilterHeadersStatus.StopIteration() + } + + override fun onResponseData( + body: ByteBuffer, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterDataStatus { + // Since we request buffering, each invocation will include all data buffered so far. + this.body = body + + // If this is the end of the stream, resume processing of the (now fully-buffered) response. + if (endStream) { + val builder = headers.toResponseHeadersBuilder() + .add("buffer-filter-demo", "1") + return FilterDataStatus.ResumeIteration(builder.build(), body) + } + return FilterDataStatus.StopIterationAndBuffer() + } + + override fun onResponseTrailers( + trailers: ResponseTrailers, + streamIntel: StreamIntel + ): FilterTrailersStatus { + // Trailers imply end of stream; resume processing of the (now fully-buffered) response. + val builder = headers.toResponseHeadersBuilder() + .add("buffer-filter-demo", "1") + return FilterTrailersStatus.ResumeIteration(builder.build(), this.body, trailers) + } + + @Suppress("EmptyFunctionBlock") + override fun onError( + error: EnvoyError, + finalStreamIntel: FinalStreamIntel + ) { + } + + @Suppress("EmptyFunctionBlock") + override fun onCancel(finalStreamIntel: FinalStreamIntel) { + } + + @Suppress("EmptyFunctionBlock") + override fun onComplete(finalStreamIntel: FinalStreamIntel) { + } +} diff --git a/test/kotlin/apps/baseline/DemoFilter.kt b/test/kotlin/apps/baseline/DemoFilter.kt new file mode 100644 index 0000000000..2f231fd130 --- /dev/null +++ b/test/kotlin/apps/baseline/DemoFilter.kt @@ -0,0 +1,58 @@ +package io.envoyproxy.envoymobile.helloenvoykotlin + +import android.util.Log +import io.envoyproxy.envoymobile.EnvoyError +import io.envoyproxy.envoymobile.FilterDataStatus +import io.envoyproxy.envoymobile.FilterHeadersStatus +import io.envoyproxy.envoymobile.FilterTrailersStatus +import io.envoyproxy.envoymobile.FinalStreamIntel +import io.envoyproxy.envoymobile.ResponseFilter +import io.envoyproxy.envoymobile.ResponseHeaders +import io.envoyproxy.envoymobile.ResponseTrailers +import io.envoyproxy.envoymobile.StreamIntel +import java.nio.ByteBuffer + +class DemoFilter : ResponseFilter { + override fun onResponseHeaders( + headers: ResponseHeaders, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterHeadersStatus { + Log.d("DemoFilter", "On headers!") + val builder = headers.toResponseHeadersBuilder() + builder.add("filter-demo", "1") + return FilterHeadersStatus.Continue(builder.build()) + } + + override fun onResponseData( + body: ByteBuffer, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterDataStatus { + Log.d("DemoFilter", "On data!") + return FilterDataStatus.Continue(body) + } + + override fun onResponseTrailers( + trailers: ResponseTrailers, + streamIntel: StreamIntel + ): FilterTrailersStatus { + Log.d("DemoFilter", "On trailers!") + return FilterTrailersStatus.Continue(trailers) + } + + override fun onError( + error: EnvoyError, + finalStreamIntel: FinalStreamIntel + ) { + Log.d("DemoFilter", "On error!") + } + + override fun onCancel(finalStreamIntel: FinalStreamIntel) { + Log.d("DemoFilter", "On cancel!") + } + + @Suppress("EmptyFunctionBlock") + override fun onComplete(finalStreamIntel: FinalStreamIntel) { + } +} diff --git a/test/kotlin/apps/baseline/MainActivity.kt b/test/kotlin/apps/baseline/MainActivity.kt new file mode 100644 index 0000000000..e66aab3793 --- /dev/null +++ b/test/kotlin/apps/baseline/MainActivity.kt @@ -0,0 +1,164 @@ +package io.envoyproxy.envoymobile.helloenvoykotlin + +import android.app.Activity +import android.os.Bundle +import android.os.Handler +import android.os.HandlerThread +import android.util.Log +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.envoyproxy.envoymobile.AndroidEngineBuilder +import io.envoyproxy.envoymobile.Element +import io.envoyproxy.envoymobile.Engine +import io.envoyproxy.envoymobile.LogLevel +import io.envoyproxy.envoymobile.RequestHeadersBuilder +import io.envoyproxy.envoymobile.RequestMethod +import io.envoyproxy.envoymobile.UpstreamHttpProtocol +import io.envoyproxy.envoymobile.shared.Failure +import io.envoyproxy.envoymobile.shared.ResponseRecyclerViewAdapter +import io.envoyproxy.envoymobile.shared.Success +import java.io.IOException +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +private const val REQUEST_HANDLER_THREAD_NAME = "hello_envoy_kt" +private const val REQUEST_AUTHORITY = "api.lyft.com" +private const val REQUEST_PATH = "/ping" +private const val REQUEST_SCHEME = "https" +private val FILTERED_HEADERS = setOf( + "server", + "filter-demo", + "buffer-filter-demo", + "async-filter-demo", + "x-envoy-upstream-service-time" +) + +class MainActivity : Activity() { + private val thread = HandlerThread(REQUEST_HANDLER_THREAD_NAME) + private lateinit var recyclerView: RecyclerView + private lateinit var viewAdapter: ResponseRecyclerViewAdapter + private lateinit var engine: Engine + + @Suppress("MaxLineLength") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + engine = AndroidEngineBuilder(application) + .addLogLevel(LogLevel.DEBUG) + .addPlatformFilter(::DemoFilter) + .addPlatformFilter(::BufferDemoFilter) + .addPlatformFilter(::AsyncDemoFilter) + .addNativeFilter("envoy.filters.http.buffer", "{\"@type\":\"type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer\",\"max_request_bytes\":5242880}") + .addStringAccessor("demo-accessor", { "PlatformString" }) + .setOnEngineRunning { Log.d("MainActivity", "Envoy async internal setup completed") } + .setEventTracker({ + for (entry in it.entries) { + Log.d("MainActivity", "Event emitted: ${entry.key}, ${entry.value}") + } + }) + .setLogger { + Log.d("MainActivity", it) + } + .build() + + recyclerView = findViewById(R.id.recycler_view) as RecyclerView + recyclerView.layoutManager = LinearLayoutManager(this) + + viewAdapter = ResponseRecyclerViewAdapter() + recyclerView.adapter = viewAdapter + val dividerItemDecoration = DividerItemDecoration( + recyclerView.context, DividerItemDecoration.VERTICAL + ) + recyclerView.addItemDecoration(dividerItemDecoration) + thread.start() + val handler = Handler(thread.looper) + + // Run a request loop and record stats until the application exits. + handler.postDelayed( + object : Runnable { + override fun run() { + try { + makeRequest() + recordStats() + } catch (e: IOException) { + Log.d("MainActivity", "exception making request or recording stats", e) + } + + // Make a call and report stats again + handler.postDelayed(this, TimeUnit.SECONDS.toMillis(1)) + } + }, + TimeUnit.SECONDS.toMillis(1) + ) + } + + override fun onDestroy() { + super.onDestroy() + thread.quit() + } + + private fun makeRequest() { + // Note: this request will use an h2 stream for the upstream request. + // The Java example uses http/1.1. This is done on purpose to test both paths in end-to-end + // tests in CI. + val requestHeaders = RequestHeadersBuilder( + RequestMethod.GET, REQUEST_SCHEME, REQUEST_AUTHORITY, REQUEST_PATH + ) + .addUpstreamHttpProtocol(UpstreamHttpProtocol.HTTP2) + .build() + engine + .streamClient() + .newStreamPrototype() + .setOnResponseHeaders { responseHeaders, _, _ -> + val status = responseHeaders.httpStatus ?: 0L + val message = "received headers with status $status" + + val sb = StringBuilder() + for ((name, value) in responseHeaders.headers) { + if (name in FILTERED_HEADERS) { + sb.append(name).append(": ").append(value.joinToString()).append("\n") + } + } + val headerText = sb.toString() + + Log.d("MainActivity", message) + responseHeaders.value("filter-demo")?.first()?.let { filterDemoValue -> + Log.d("MainActivity", "filter-demo: $filterDemoValue") + } + + if (status == 200) { + recyclerView.post { viewAdapter.add(Success(message, headerText)) } + } else { + recyclerView.post { viewAdapter.add(Failure(message)) } + } + } + .setOnError { error, _ -> + val attemptCount = error.attemptCount ?: -1 + val message = "failed with error after $attemptCount attempts: ${error.message}" + Log.d("MainActivity", message) + recyclerView.post { viewAdapter.add(Failure(message)) } + } + .start(Executors.newSingleThreadExecutor()) + .sendHeaders(requestHeaders, true) + } + + private fun recordStats() { + val counter = engine.pulseClient().counter(Element("foo"), Element("bar"), Element("counter")) + val gauge = engine.pulseClient().gauge(Element("foo"), Element("bar"), Element("gauge")) + val timer = engine.pulseClient().timer(Element("foo"), Element("bar"), Element("timer")) + val distribution = + engine.pulseClient().distribution(Element("foo"), Element("bar"), Element("distribution")) + + counter.increment() + counter.increment(5) + + gauge.set(5) + gauge.add(10) + gauge.sub(1) + + timer.recordDuration(15) + distribution.recordValue(15) + } +} diff --git a/test/kotlin/apps/baseline/README.md b/test/kotlin/apps/baseline/README.md new file mode 100644 index 0000000000..53e8270fb7 --- /dev/null +++ b/test/kotlin/apps/baseline/README.md @@ -0,0 +1 @@ +For instructions on how to use this demo, please head over to our [docs](https://envoy-mobile.github.io/docs/envoy-mobile/latest/start/examples/hello_world.html). diff --git a/test/kotlin/apps/baseline/res/layout/activity_main.xml b/test/kotlin/apps/baseline/res/layout/activity_main.xml new file mode 100644 index 0000000000..0d8c4ff3fd --- /dev/null +++ b/test/kotlin/apps/baseline/res/layout/activity_main.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/test/kotlin/apps/baseline/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml b/test/kotlin/apps/baseline/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml new file mode 100644 index 0000000000..ab975a532c --- /dev/null +++ b/test/kotlin/apps/baseline/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml @@ -0,0 +1,38 @@ + + + --fat_apk_cpu=x86 + //examples/kotlin/hello_world:hello_envoy_kt + + + + + + diff --git a/test/kotlin/apps/experimental/.bazelproject b/test/kotlin/apps/experimental/.bazelproject new file mode 100644 index 0000000000..651096b4a0 --- /dev/null +++ b/test/kotlin/apps/experimental/.bazelproject @@ -0,0 +1,22 @@ +workspace_type: android + +directories: + -bazel-bin + -bazel-instant-android + -bazel-out + -bazel-testlogs + -buck-out + -build + examples/kotlin/hello_world + +import_run_configurations: + examples/kotlin/hello_world/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml + +targets: + //examples/kotlin/hello_world:hello_envoy_kt + +additional_languages: + kotlin + java + android + c diff --git a/test/kotlin/apps/experimental/AndroidManifest.xml b/test/kotlin/apps/experimental/AndroidManifest.xml new file mode 100644 index 0000000000..89427cc582 --- /dev/null +++ b/test/kotlin/apps/experimental/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/test/kotlin/apps/experimental/AsyncDemoFilter.kt b/test/kotlin/apps/experimental/AsyncDemoFilter.kt new file mode 100644 index 0000000000..a45ec5f7af --- /dev/null +++ b/test/kotlin/apps/experimental/AsyncDemoFilter.kt @@ -0,0 +1,95 @@ +package io.envoyproxy.envoymobile.helloenvoykotlin + +import io.envoyproxy.envoymobile.AsyncResponseFilter +import io.envoyproxy.envoymobile.EnvoyError +import io.envoyproxy.envoymobile.FilterDataStatus +import io.envoyproxy.envoymobile.FilterHeadersStatus +import io.envoyproxy.envoymobile.FilterResumeStatus +import io.envoyproxy.envoymobile.FilterTrailersStatus +import io.envoyproxy.envoymobile.FinalStreamIntel +import io.envoyproxy.envoymobile.ResponseFilterCallbacks +import io.envoyproxy.envoymobile.ResponseHeaders +import io.envoyproxy.envoymobile.ResponseTrailers +import io.envoyproxy.envoymobile.StreamIntel +import java.nio.ByteBuffer +import java.util.Timer +import kotlin.concurrent.schedule + +/** + * Example of a more complex HTTP filter that pauses processing on the response filter chain, + * buffers until the response is complete, then asynchronously triggers filter chain resumption + * while setting a new header. Also demonstrates safety of re-entrancy in async callbacks. + */ +class AsyncDemoFilter : AsyncResponseFilter { + private lateinit var callbacks: ResponseFilterCallbacks + + override fun onResponseHeaders( + headers: ResponseHeaders, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterHeadersStatus { + // If this is the end of the stream, asynchronously resume response processing via callback. + if (endStream) { + Timer("AsyncResume", false).schedule(100) { + callbacks.resumeResponse() + } + } + return FilterHeadersStatus.StopIteration() + } + + override fun onResponseData( + body: ByteBuffer, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterDataStatus { + // If this is the end of the stream, asynchronously resume response processing via callback. + if (endStream) { + Timer("AsyncResume", false).schedule(100) { + callbacks.resumeResponse() + } + } + return FilterDataStatus.StopIterationAndBuffer() + } + + override fun onResponseTrailers( + trailers: ResponseTrailers, + streamIntel: StreamIntel + ): FilterTrailersStatus { + // Trailers imply end of stream, so asynchronously resume response processing via callbacka + Timer("AsyncResume", false).schedule(100) { + callbacks.resumeResponse() + } + return FilterTrailersStatus.StopIteration() + } + + override fun setResponseFilterCallbacks(callbacks: ResponseFilterCallbacks) { + this.callbacks = callbacks + } + + override fun onResumeResponse( + headers: ResponseHeaders?, + data: ByteBuffer?, + trailers: ResponseTrailers?, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterResumeStatus { + val builder = headers!!.toResponseHeadersBuilder() + .add("async-filter-demo", "1") + return FilterResumeStatus.ResumeIteration(builder.build(), data, trailers) + } + + @Suppress("EmptyFunctionBlock") + override fun onError( + error: EnvoyError, + finalStreamIntel: FinalStreamIntel + ) { + } + + @Suppress("EmptyFunctionBlock") + override fun onCancel(finalStreamIntel: FinalStreamIntel) { + } + + @Suppress("EmptyFunctionBlock") + override fun onComplete(finalStreamIntel: FinalStreamIntel) { + } +} diff --git a/test/kotlin/apps/experimental/BUILD b/test/kotlin/apps/experimental/BUILD new file mode 100644 index 0000000000..7ef9cb46c2 --- /dev/null +++ b/test/kotlin/apps/experimental/BUILD @@ -0,0 +1,45 @@ +load("@build_bazel_rules_android//android:rules.bzl", "android_binary") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") +load("@rules_detekt//detekt:defs.bzl", "detekt") +load("@rules_jvm_external//:defs.bzl", "artifact") + +licenses(["notice"]) # Apache 2 + +android_binary( + name = "hello_envoy_kt", + custom_package = "io.envoyproxy.envoymobile.helloenvoykotlin", + manifest = "AndroidManifest.xml", + proguard_specs = ["//library:proguard_rules"], + deps = [ + "hello_envoy_kt_lib", + ], +) + +kt_android_library( + name = "hello_envoy_kt_lib", + srcs = [ + "AsyncDemoFilter.kt", + "BufferDemoFilter.kt", + "DemoFilter.kt", + "MainActivity.kt", + ], + custom_package = "io.envoyproxy.envoymobile.helloenvoykotlin", + manifest = "AndroidManifest.xml", + resource_files = [ + "res/layout/activity_main.xml", + ], + deps = [ + "//dist:envoy_mobile_android", + "//examples/kotlin/shared:hello_envoy_shared_lib", + artifact("androidx.recyclerview:recyclerview"), + artifact("androidx.annotation:annotation"), + artifact("com.google.code.findbugs:jsr305"), + ], +) + +detekt( + name = "hello_envoy_kt_lint", + srcs = glob(["*.kt"]), + build_upon_default_config = True, + cfgs = ["//:kotlin_lint_config"], +) diff --git a/test/kotlin/apps/experimental/BufferDemoFilter.kt b/test/kotlin/apps/experimental/BufferDemoFilter.kt new file mode 100644 index 0000000000..f158d1b1e6 --- /dev/null +++ b/test/kotlin/apps/experimental/BufferDemoFilter.kt @@ -0,0 +1,73 @@ +package io.envoyproxy.envoymobile.helloenvoykotlin + +import io.envoyproxy.envoymobile.EnvoyError +import io.envoyproxy.envoymobile.FilterDataStatus +import io.envoyproxy.envoymobile.FilterHeadersStatus +import io.envoyproxy.envoymobile.FilterTrailersStatus +import io.envoyproxy.envoymobile.FinalStreamIntel +import io.envoyproxy.envoymobile.ResponseFilter +import io.envoyproxy.envoymobile.ResponseHeaders +import io.envoyproxy.envoymobile.ResponseTrailers +import io.envoyproxy.envoymobile.StreamIntel +import java.nio.ByteBuffer + +/** + * Example of a more complex HTTP filter that pauses processing on the response filter chain, + * buffers until the response is complete, then resumes filter iteration while setting a new + * header. + */ +class BufferDemoFilter : ResponseFilter { + private lateinit var headers: ResponseHeaders + private lateinit var body: ByteBuffer + + override fun onResponseHeaders( + headers: ResponseHeaders, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterHeadersStatus { + this.headers = headers + return FilterHeadersStatus.StopIteration() + } + + override fun onResponseData( + body: ByteBuffer, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterDataStatus { + // Since we request buffering, each invocation will include all data buffered so far. + this.body = body + + // If this is the end of the stream, resume processing of the (now fully-buffered) response. + if (endStream) { + val builder = headers.toResponseHeadersBuilder() + .add("buffer-filter-demo", "1") + return FilterDataStatus.ResumeIteration(builder.build(), body) + } + return FilterDataStatus.StopIterationAndBuffer() + } + + override fun onResponseTrailers( + trailers: ResponseTrailers, + streamIntel: StreamIntel + ): FilterTrailersStatus { + // Trailers imply end of stream; resume processing of the (now fully-buffered) response. + val builder = headers.toResponseHeadersBuilder() + .add("buffer-filter-demo", "1") + return FilterTrailersStatus.ResumeIteration(builder.build(), this.body, trailers) + } + + @Suppress("EmptyFunctionBlock") + override fun onError( + error: EnvoyError, + finalStreamIntel: FinalStreamIntel + ) { + } + + @Suppress("EmptyFunctionBlock") + override fun onCancel(finalStreamIntel: FinalStreamIntel) { + } + + @Suppress("EmptyFunctionBlock") + override fun onComplete(finalStreamIntel: FinalStreamIntel) { + } +} diff --git a/test/kotlin/apps/experimental/DemoFilter.kt b/test/kotlin/apps/experimental/DemoFilter.kt new file mode 100644 index 0000000000..2f231fd130 --- /dev/null +++ b/test/kotlin/apps/experimental/DemoFilter.kt @@ -0,0 +1,58 @@ +package io.envoyproxy.envoymobile.helloenvoykotlin + +import android.util.Log +import io.envoyproxy.envoymobile.EnvoyError +import io.envoyproxy.envoymobile.FilterDataStatus +import io.envoyproxy.envoymobile.FilterHeadersStatus +import io.envoyproxy.envoymobile.FilterTrailersStatus +import io.envoyproxy.envoymobile.FinalStreamIntel +import io.envoyproxy.envoymobile.ResponseFilter +import io.envoyproxy.envoymobile.ResponseHeaders +import io.envoyproxy.envoymobile.ResponseTrailers +import io.envoyproxy.envoymobile.StreamIntel +import java.nio.ByteBuffer + +class DemoFilter : ResponseFilter { + override fun onResponseHeaders( + headers: ResponseHeaders, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterHeadersStatus { + Log.d("DemoFilter", "On headers!") + val builder = headers.toResponseHeadersBuilder() + builder.add("filter-demo", "1") + return FilterHeadersStatus.Continue(builder.build()) + } + + override fun onResponseData( + body: ByteBuffer, + endStream: Boolean, + streamIntel: StreamIntel + ): FilterDataStatus { + Log.d("DemoFilter", "On data!") + return FilterDataStatus.Continue(body) + } + + override fun onResponseTrailers( + trailers: ResponseTrailers, + streamIntel: StreamIntel + ): FilterTrailersStatus { + Log.d("DemoFilter", "On trailers!") + return FilterTrailersStatus.Continue(trailers) + } + + override fun onError( + error: EnvoyError, + finalStreamIntel: FinalStreamIntel + ) { + Log.d("DemoFilter", "On error!") + } + + override fun onCancel(finalStreamIntel: FinalStreamIntel) { + Log.d("DemoFilter", "On cancel!") + } + + @Suppress("EmptyFunctionBlock") + override fun onComplete(finalStreamIntel: FinalStreamIntel) { + } +} diff --git a/test/kotlin/apps/experimental/MainActivity.kt b/test/kotlin/apps/experimental/MainActivity.kt new file mode 100644 index 0000000000..e99b7295ca --- /dev/null +++ b/test/kotlin/apps/experimental/MainActivity.kt @@ -0,0 +1,166 @@ +package io.envoyproxy.envoymobile.helloenvoykotlin + +import android.app.Activity +import android.os.Bundle +import android.os.Handler +import android.os.HandlerThread +import android.util.Log +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.envoyproxy.envoymobile.AndroidEngineBuilder +import io.envoyproxy.envoymobile.Element +import io.envoyproxy.envoymobile.Engine +import io.envoyproxy.envoymobile.LogLevel +import io.envoyproxy.envoymobile.RequestHeadersBuilder +import io.envoyproxy.envoymobile.RequestMethod +import io.envoyproxy.envoymobile.UpstreamHttpProtocol +import io.envoyproxy.envoymobile.shared.Failure +import io.envoyproxy.envoymobile.shared.ResponseRecyclerViewAdapter +import io.envoyproxy.envoymobile.shared.Success +import java.io.IOException +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +private const val REQUEST_HANDLER_THREAD_NAME = "hello_envoy_kt" +private const val REQUEST_AUTHORITY = "api.lyft.com" +private const val REQUEST_PATH = "/ping" +private const val REQUEST_SCHEME = "https" +private val FILTERED_HEADERS = setOf( + "server", + "filter-demo", + "buffer-filter-demo", + "async-filter-demo", + "x-envoy-upstream-service-time" +) + +class MainActivity : Activity() { + private val thread = HandlerThread(REQUEST_HANDLER_THREAD_NAME) + private lateinit var recyclerView: RecyclerView + private lateinit var viewAdapter: ResponseRecyclerViewAdapter + private lateinit var engine: Engine + + @Suppress("MaxLineLength") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + engine = AndroidEngineBuilder(application) + .addLogLevel(LogLevel.DEBUG) + .addPlatformFilter(::DemoFilter) + .addPlatformFilter(::BufferDemoFilter) + .addPlatformFilter(::AsyncDemoFilter) + .enableHappyEyeballs(true) + .enableInterfaceBinding(true) + .addNativeFilter("envoy.filters.http.buffer", "{\"@type\":\"type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer\",\"max_request_bytes\":5242880}") + .addStringAccessor("demo-accessor", { "PlatformString" }) + .setOnEngineRunning { Log.d("MainActivity", "Envoy async internal setup completed") } + .setEventTracker({ + for (entry in it.entries) { + Log.d("MainActivity", "Event emitted: ${entry.key}, ${entry.value}") + } + }) + .setLogger { + Log.d("MainActivity", it) + } + .build() + + recyclerView = findViewById(R.id.recycler_view) as RecyclerView + recyclerView.layoutManager = LinearLayoutManager(this) + + viewAdapter = ResponseRecyclerViewAdapter() + recyclerView.adapter = viewAdapter + val dividerItemDecoration = DividerItemDecoration( + recyclerView.context, DividerItemDecoration.VERTICAL + ) + recyclerView.addItemDecoration(dividerItemDecoration) + thread.start() + val handler = Handler(thread.looper) + + // Run a request loop and record stats until the application exits. + handler.postDelayed( + object : Runnable { + override fun run() { + try { + makeRequest() + recordStats() + } catch (e: IOException) { + Log.d("MainActivity", "exception making request or recording stats", e) + } + + // Make a call and report stats again + handler.postDelayed(this, TimeUnit.SECONDS.toMillis(1)) + } + }, + TimeUnit.SECONDS.toMillis(1) + ) + } + + override fun onDestroy() { + super.onDestroy() + thread.quit() + } + + private fun makeRequest() { + // Note: this request will use an h2 stream for the upstream request. + // The Java example uses http/1.1. This is done on purpose to test both paths in end-to-end + // tests in CI. + val requestHeaders = RequestHeadersBuilder( + RequestMethod.GET, REQUEST_SCHEME, REQUEST_AUTHORITY, REQUEST_PATH + ) + .addUpstreamHttpProtocol(UpstreamHttpProtocol.HTTP2) + .build() + engine + .streamClient() + .newStreamPrototype() + .setOnResponseHeaders { responseHeaders, _, _ -> + val status = responseHeaders.httpStatus ?: 0L + val message = "received headers with status $status" + + val sb = StringBuilder() + for ((name, value) in responseHeaders.headers) { + if (name in FILTERED_HEADERS) { + sb.append(name).append(": ").append(value.joinToString()).append("\n") + } + } + val headerText = sb.toString() + + Log.d("MainActivity", message) + responseHeaders.value("filter-demo")?.first()?.let { filterDemoValue -> + Log.d("MainActivity", "filter-demo: $filterDemoValue") + } + + if (status == 200) { + recyclerView.post { viewAdapter.add(Success(message, headerText)) } + } else { + recyclerView.post { viewAdapter.add(Failure(message)) } + } + } + .setOnError { error, _ -> + val attemptCount = error.attemptCount ?: -1 + val message = "failed with error after $attemptCount attempts: ${error.message}" + Log.d("MainActivity", message) + recyclerView.post { viewAdapter.add(Failure(message)) } + } + .start(Executors.newSingleThreadExecutor()) + .sendHeaders(requestHeaders, true) + } + + private fun recordStats() { + val counter = engine.pulseClient().counter(Element("foo"), Element("bar"), Element("counter")) + val gauge = engine.pulseClient().gauge(Element("foo"), Element("bar"), Element("gauge")) + val timer = engine.pulseClient().timer(Element("foo"), Element("bar"), Element("timer")) + val distribution = + engine.pulseClient().distribution(Element("foo"), Element("bar"), Element("distribution")) + + counter.increment() + counter.increment(5) + + gauge.set(5) + gauge.add(10) + gauge.sub(1) + + timer.recordDuration(15) + distribution.recordValue(15) + } +} diff --git a/test/kotlin/apps/experimental/README.md b/test/kotlin/apps/experimental/README.md new file mode 100644 index 0000000000..53e8270fb7 --- /dev/null +++ b/test/kotlin/apps/experimental/README.md @@ -0,0 +1 @@ +For instructions on how to use this demo, please head over to our [docs](https://envoy-mobile.github.io/docs/envoy-mobile/latest/start/examples/hello_world.html). diff --git a/test/kotlin/apps/experimental/res/layout/activity_main.xml b/test/kotlin/apps/experimental/res/layout/activity_main.xml new file mode 100644 index 0000000000..0d8c4ff3fd --- /dev/null +++ b/test/kotlin/apps/experimental/res/layout/activity_main.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/test/kotlin/apps/experimental/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml b/test/kotlin/apps/experimental/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml new file mode 100644 index 0000000000..ab975a532c --- /dev/null +++ b/test/kotlin/apps/experimental/tools/android-studio-run-configurations/run_configuration_example_debug_x86.xml @@ -0,0 +1,38 @@ + + + --fat_apk_cpu=x86 + //examples/kotlin/hello_world:hello_envoy_kt + + + + + + diff --git a/test/swift/apps/baseline/AppDelegate.swift b/test/swift/apps/baseline/AppDelegate.swift new file mode 100644 index 0000000000..2f161c9b3e --- /dev/null +++ b/test/swift/apps/baseline/AppDelegate.swift @@ -0,0 +1,19 @@ +import UIKit + +@UIApplicationMain +final class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool + { + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = ViewController() + window.makeKeyAndVisible() + self.window = window + + NSLog("Finished launching!") + return true + } +} diff --git a/test/swift/apps/baseline/AsyncDemoFilter.swift b/test/swift/apps/baseline/AsyncDemoFilter.swift new file mode 100644 index 0000000000..c25e10ef43 --- /dev/null +++ b/test/swift/apps/baseline/AsyncDemoFilter.swift @@ -0,0 +1,67 @@ +import Envoy +import Foundation + +/// Example of a more complex HTTP filter that pauses processing on the response filter chain, +/// buffers until the response is complete, then asynchronously triggers filter chain resumption +/// while setting a new header. Also demonstrates safety of re-entrancy of async callbacks. +final class AsyncDemoFilter: AsyncResponseFilter { + private var callbacks: ResponseFilterCallbacks! + + func onResponseHeaders(_ headers: ResponseHeaders, endStream: Bool, streamIntel: StreamIntel) + -> FilterHeadersStatus + { + if endStream { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.callbacks.resumeResponse() + } + } + return .stopIteration + } + + func onResponseData(_ body: Data, endStream: Bool, streamIntel: StreamIntel) + -> FilterDataStatus + { + if endStream { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.callbacks.resumeResponse() + } + } + return .stopIterationAndBuffer + } + + func onResponseTrailers( + _ trailers: ResponseTrailers, + streamIntel: StreamIntel + ) -> FilterTrailersStatus { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.callbacks.resumeResponse() + } + return .stopIteration + } + + func setResponseFilterCallbacks(_ callbacks: ResponseFilterCallbacks) { + self.callbacks = callbacks + } + + func onResumeResponse( + headers: ResponseHeaders?, + data: Data?, + trailers: ResponseTrailers?, + endStream: Bool, + streamIntel: StreamIntel + ) -> FilterResumeStatus { + guard let headers = headers else { + // Iteration was stopped on headers, so headers must be present. + fatalError("Filter behavior violation!") + } + let builder = headers.toResponseHeadersBuilder() + .add(name: "async-filter-demo", value: "1") + return .resumeIteration(headers: builder.build(), data: data, trailers: trailers) + } + + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {} + + func onCancel(streamIntel: FinalStreamIntel) {} + + func onComplete(streamIntel: FinalStreamIntel) {} +} diff --git a/test/swift/apps/baseline/BUILD b/test/swift/apps/baseline/BUILD new file mode 100644 index 0000000000..c4f17ed7e4 --- /dev/null +++ b/test/swift/apps/baseline/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application") +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +licenses(["notice"]) # Apache 2 + +swift_library( + name = "appmain", + srcs = glob(["*.swift"]), + deps = ["//dist:envoy_mobile_ios"], +) + +ios_application( + name = "app", + bundle_id = "io.envoyproxy.envoymobile.helloworld", + families = ["iphone"], + infoplists = ["Info.plist"], + minimum_os_version = "11.0", + deps = ["appmain"], +) diff --git a/test/swift/apps/baseline/Base.lproj/LaunchScreen.storyboard b/test/swift/apps/baseline/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..bfa3612941 --- /dev/null +++ b/test/swift/apps/baseline/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/swift/apps/baseline/BufferDemoFilter.swift b/test/swift/apps/baseline/BufferDemoFilter.swift new file mode 100644 index 0000000000..1def3fb677 --- /dev/null +++ b/test/swift/apps/baseline/BufferDemoFilter.swift @@ -0,0 +1,48 @@ +import Envoy +import Foundation + +/// Example of a more complex HTTP filter that pauses processing on the response filter chain, +/// buffers until the response is complete, then resumes filter iteration while setting a new +/// header. +final class BufferDemoFilter: ResponseFilter { + private var headers: ResponseHeaders! + private var body: Data? + + func onResponseHeaders(_ headers: ResponseHeaders, endStream: Bool, streamIntel: StreamIntel) + -> FilterHeadersStatus + { + self.headers = headers + return .stopIteration + } + + func onResponseData(_ data: Data, endStream: Bool, streamIntel: StreamIntel) + -> FilterDataStatus + { + // Since we request buffering, each invocation will include all data buffered so far. + self.body = data + + // If this is the end of the stream, resume processing of the (now fully-buffered) response. + if endStream { + let builder = self.headers.toResponseHeadersBuilder() + .add(name: "buffer-filter-demo", value: "1") + return .resumeIteration(headers: builder.build(), data: data) + } + return .stopIterationAndBuffer + } + + func onResponseTrailers( + _ trailers: ResponseTrailers, + streamIntel: StreamIntel + ) -> FilterTrailersStatus { + // Trailers imply end of stream; resume processing of the (now fully-buffered) response. + let builder = self.headers.toResponseHeadersBuilder() + .add(name: "buffer-filter-demo", value: "1") + return .resumeIteration(headers: builder.build(), data: self.body, trailers: trailers) + } + + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {} + + func onCancel(streamIntel: FinalStreamIntel) {} + + func onComplete(streamIntel: FinalStreamIntel) {} +} diff --git a/test/swift/apps/baseline/DemoFilter.swift b/test/swift/apps/baseline/DemoFilter.swift new file mode 100644 index 0000000000..27e79c3e6b --- /dev/null +++ b/test/swift/apps/baseline/DemoFilter.swift @@ -0,0 +1,33 @@ +import Envoy +import Foundation + +/// Example of a simple HTTP filter that adds a response header. +struct DemoFilter: ResponseFilter { + func onResponseHeaders(_ headers: ResponseHeaders, endStream: Bool, streamIntel: StreamIntel) + -> FilterHeadersStatus + { + let builder = headers.toResponseHeadersBuilder() + builder.add(name: "filter-demo", value: "1") + return .continue(headers: builder.build()) + } + + func setResponseFilterCallbacks(_ callbacks: ResponseFilterCallbacks, streamIntel: StreamIntel) {} + + func onResponseData(_ body: Data, endStream: Bool, streamIntel: StreamIntel) + -> FilterDataStatus + { + return .continue(data: body) + } + + func onResponseTrailers(_ trailers: ResponseTrailers, streamIntel: StreamIntel) + -> FilterTrailersStatus + { + return .continue(trailers: trailers) + } + + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {} + + func onCancel(streamIntel: FinalStreamIntel) {} + + func onComplete(streamIntel: FinalStreamIntel) {} +} diff --git a/test/swift/apps/baseline/Info.plist b/test/swift/apps/baseline/Info.plist new file mode 100644 index 0000000000..83af5464ce --- /dev/null +++ b/test/swift/apps/baseline/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1 + CFBundleSignature + ???? + CFBundleVersion + 0.1 + ITSAppUsesNonExemptEncryption + + NSAppTransportSecurity + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarStyle + UIStatusBarStyleLightContent + UILaunchStoryboardName + LaunchScreen + + diff --git a/test/swift/apps/baseline/README.md b/test/swift/apps/baseline/README.md new file mode 100644 index 0000000000..53e8270fb7 --- /dev/null +++ b/test/swift/apps/baseline/README.md @@ -0,0 +1 @@ +For instructions on how to use this demo, please head over to our [docs](https://envoy-mobile.github.io/docs/envoy-mobile/latest/start/examples/hello_world.html). diff --git a/test/swift/apps/baseline/ResponseModels.swift b/test/swift/apps/baseline/ResponseModels.swift new file mode 100644 index 0000000000..c83e555320 --- /dev/null +++ b/test/swift/apps/baseline/ResponseModels.swift @@ -0,0 +1,10 @@ +/// Represents a response from the server. +struct Response { + let message: String + let headerMessage: String +} + +/// Error that was encountered when executing a request. +struct RequestError: Error { + let message: String +} diff --git a/test/swift/apps/baseline/ViewController.swift b/test/swift/apps/baseline/ViewController.swift new file mode 100644 index 0000000000..8676c01002 --- /dev/null +++ b/test/swift/apps/baseline/ViewController.swift @@ -0,0 +1,168 @@ +import Envoy +import UIKit + +private let kCellID = "cell-id" +private let kRequestAuthority = "api.lyft.com" +private let kRequestPath = "/ping" +private let kRequestScheme = "https" +private let kFilteredHeaders = + ["server", "filter-demo", "async-filter-demo", "x-envoy-upstream-service-time"] + +final class ViewController: UITableViewController { + private var results = [Result]() + private var timer: Foundation.Timer? + private var streamClient: StreamClient? + private var pulseClient: PulseClient? + + override func viewDidLoad() { + super.viewDidLoad() + + let engine = EngineBuilder() + .addLogLevel(.debug) + .addPlatformFilter(DemoFilter.init) + .addPlatformFilter(BufferDemoFilter.init) + .addPlatformFilter(AsyncDemoFilter.init) + // swiftlint:disable:next line_length + .addNativeFilter(name: "envoy.filters.http.buffer", typedConfig: "{\"@type\":\"type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer\",\"max_request_bytes\":5242880}") + .setOnEngineRunning { NSLog("Envoy async internal setup completed") } + .addStringAccessor(name: "demo-accessor", accessor: { return "PlatformString" }) + .setEventTracker { NSLog("Envoy event emitted: \($0)") } + .build() + self.streamClient = engine.streamClient() + self.pulseClient = engine.pulseClient() + + NSLog("started Envoy, beginning requests...") + self.startRequests() + } + + deinit { + self.timer?.invalidate() + } + + // MARK: - Requests + + private func startRequests() { + self.timer = .scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + self?.performRequest() + self?.recordStats() + } + } + + private func performRequest() { + guard let streamClient = self.streamClient else { + NSLog("failed to start request - Envoy is not running") + return + } + + NSLog("starting request to '\(kRequestPath)'") + + // Note: this request will use an h2 stream for the upstream request. + // The Objective-C example uses http/1.1. This is done on purpose to test both paths in + // end-to-end tests in CI. + let headers = RequestHeadersBuilder(method: .get, scheme: kRequestScheme, + authority: kRequestAuthority, path: kRequestPath) + .addUpstreamHttpProtocol(.http2) + .build() + + streamClient + .newStreamPrototype() + .setOnResponseHeaders { [weak self] headers, _, _ in + let statusCode = headers.httpStatus.map(String.init) ?? "nil" + let message = "received headers with status \(statusCode)" + + let headerMessage = headers.allHeaders() + .filter { kFilteredHeaders.contains($0.key) } + .map { "\($0.key): \($0.value.joined(separator: ", "))" } + .joined(separator: "\n") + + NSLog(message) + if let filterDemoValue = headers.value(forName: "filter-demo")?.first { + NSLog("filter-demo: \(filterDemoValue)") + } + if let asyncFilterDemoValue = headers.value(forName: "async-filter-demo")?.first { + NSLog("async-filter-demo: \(asyncFilterDemoValue)") + } + + let response = Response(message: message, + headerMessage: headerMessage) + self?.add(result: .success(response)) + } + .setOnError { [weak self] error, _ in + let message: String + if let attemptCount = error.attemptCount { + message = "failed within Envoy library after \(attemptCount) attempts: \(error.message)" + } else { + message = "failed within Envoy library: \(error.message)" + } + + NSLog(message) + self?.add(result: .failure(RequestError(message: message))) + } + .start() + .sendHeaders(headers, endStream: true) + } + + private func add(result: Result) { + self.results.insert(result, at: 0) + self.tableView.reloadData() + } + + private func recordStats() { + guard let pulseClient = self.pulseClient else { + NSLog("failed to send stats - Envoy is not running") + return + } + + let counter = pulseClient.counter(elements: ["foo", "bar", "counter"]) + counter.increment() + counter.increment(count: 5) + + let gauge = pulseClient.gauge(elements: ["foo", "bar", "gauge"]) + gauge.set(value: 5) + gauge.add(amount: 10) + gauge.sub(amount: 1) + + let timer = pulseClient.timer(elements: ["foo", "bar", "timer"]) + let distribution = pulseClient.distribution(elements: ["foo", "bar", "distribution"]) + timer.recordDuration(durationMs: 15) + distribution.recordValue(value: 15) + } + // MARK: - UITableView + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.results.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) + -> UITableViewCell + { + let cell = tableView.dequeueReusableCell(withIdentifier: kCellID) ?? + UITableViewCell(style: .subtitle, reuseIdentifier: kCellID) + + let result = self.results[indexPath.row] + switch result { + case .success(let response): + cell.textLabel?.text = response.message + cell.detailTextLabel?.text = response.headerMessage + + cell.textLabel?.textColor = .black + cell.detailTextLabel?.lineBreakMode = .byWordWrapping + cell.detailTextLabel?.numberOfLines = 0 + cell.detailTextLabel?.textColor = .black + cell.contentView.backgroundColor = .white + case .failure(let error): + cell.textLabel?.text = error.message + cell.detailTextLabel?.text = nil + + cell.textLabel?.textColor = .white + cell.detailTextLabel?.textColor = .white + cell.contentView.backgroundColor = .red + } + + return cell + } +} diff --git a/test/swift/apps/experimental/AppDelegate.swift b/test/swift/apps/experimental/AppDelegate.swift new file mode 100644 index 0000000000..2f161c9b3e --- /dev/null +++ b/test/swift/apps/experimental/AppDelegate.swift @@ -0,0 +1,19 @@ +import UIKit + +@UIApplicationMain +final class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool + { + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = ViewController() + window.makeKeyAndVisible() + self.window = window + + NSLog("Finished launching!") + return true + } +} diff --git a/test/swift/apps/experimental/AsyncDemoFilter.swift b/test/swift/apps/experimental/AsyncDemoFilter.swift new file mode 100644 index 0000000000..c25e10ef43 --- /dev/null +++ b/test/swift/apps/experimental/AsyncDemoFilter.swift @@ -0,0 +1,67 @@ +import Envoy +import Foundation + +/// Example of a more complex HTTP filter that pauses processing on the response filter chain, +/// buffers until the response is complete, then asynchronously triggers filter chain resumption +/// while setting a new header. Also demonstrates safety of re-entrancy of async callbacks. +final class AsyncDemoFilter: AsyncResponseFilter { + private var callbacks: ResponseFilterCallbacks! + + func onResponseHeaders(_ headers: ResponseHeaders, endStream: Bool, streamIntel: StreamIntel) + -> FilterHeadersStatus + { + if endStream { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.callbacks.resumeResponse() + } + } + return .stopIteration + } + + func onResponseData(_ body: Data, endStream: Bool, streamIntel: StreamIntel) + -> FilterDataStatus + { + if endStream { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.callbacks.resumeResponse() + } + } + return .stopIterationAndBuffer + } + + func onResponseTrailers( + _ trailers: ResponseTrailers, + streamIntel: StreamIntel + ) -> FilterTrailersStatus { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.callbacks.resumeResponse() + } + return .stopIteration + } + + func setResponseFilterCallbacks(_ callbacks: ResponseFilterCallbacks) { + self.callbacks = callbacks + } + + func onResumeResponse( + headers: ResponseHeaders?, + data: Data?, + trailers: ResponseTrailers?, + endStream: Bool, + streamIntel: StreamIntel + ) -> FilterResumeStatus { + guard let headers = headers else { + // Iteration was stopped on headers, so headers must be present. + fatalError("Filter behavior violation!") + } + let builder = headers.toResponseHeadersBuilder() + .add(name: "async-filter-demo", value: "1") + return .resumeIteration(headers: builder.build(), data: data, trailers: trailers) + } + + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {} + + func onCancel(streamIntel: FinalStreamIntel) {} + + func onComplete(streamIntel: FinalStreamIntel) {} +} diff --git a/test/swift/apps/experimental/BUILD b/test/swift/apps/experimental/BUILD new file mode 100644 index 0000000000..c4f17ed7e4 --- /dev/null +++ b/test/swift/apps/experimental/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application") +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +licenses(["notice"]) # Apache 2 + +swift_library( + name = "appmain", + srcs = glob(["*.swift"]), + deps = ["//dist:envoy_mobile_ios"], +) + +ios_application( + name = "app", + bundle_id = "io.envoyproxy.envoymobile.helloworld", + families = ["iphone"], + infoplists = ["Info.plist"], + minimum_os_version = "11.0", + deps = ["appmain"], +) diff --git a/test/swift/apps/experimental/Base.lproj/LaunchScreen.storyboard b/test/swift/apps/experimental/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..bfa3612941 --- /dev/null +++ b/test/swift/apps/experimental/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/swift/apps/experimental/BufferDemoFilter.swift b/test/swift/apps/experimental/BufferDemoFilter.swift new file mode 100644 index 0000000000..1def3fb677 --- /dev/null +++ b/test/swift/apps/experimental/BufferDemoFilter.swift @@ -0,0 +1,48 @@ +import Envoy +import Foundation + +/// Example of a more complex HTTP filter that pauses processing on the response filter chain, +/// buffers until the response is complete, then resumes filter iteration while setting a new +/// header. +final class BufferDemoFilter: ResponseFilter { + private var headers: ResponseHeaders! + private var body: Data? + + func onResponseHeaders(_ headers: ResponseHeaders, endStream: Bool, streamIntel: StreamIntel) + -> FilterHeadersStatus + { + self.headers = headers + return .stopIteration + } + + func onResponseData(_ data: Data, endStream: Bool, streamIntel: StreamIntel) + -> FilterDataStatus + { + // Since we request buffering, each invocation will include all data buffered so far. + self.body = data + + // If this is the end of the stream, resume processing of the (now fully-buffered) response. + if endStream { + let builder = self.headers.toResponseHeadersBuilder() + .add(name: "buffer-filter-demo", value: "1") + return .resumeIteration(headers: builder.build(), data: data) + } + return .stopIterationAndBuffer + } + + func onResponseTrailers( + _ trailers: ResponseTrailers, + streamIntel: StreamIntel + ) -> FilterTrailersStatus { + // Trailers imply end of stream; resume processing of the (now fully-buffered) response. + let builder = self.headers.toResponseHeadersBuilder() + .add(name: "buffer-filter-demo", value: "1") + return .resumeIteration(headers: builder.build(), data: self.body, trailers: trailers) + } + + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {} + + func onCancel(streamIntel: FinalStreamIntel) {} + + func onComplete(streamIntel: FinalStreamIntel) {} +} diff --git a/test/swift/apps/experimental/DemoFilter.swift b/test/swift/apps/experimental/DemoFilter.swift new file mode 100644 index 0000000000..27e79c3e6b --- /dev/null +++ b/test/swift/apps/experimental/DemoFilter.swift @@ -0,0 +1,33 @@ +import Envoy +import Foundation + +/// Example of a simple HTTP filter that adds a response header. +struct DemoFilter: ResponseFilter { + func onResponseHeaders(_ headers: ResponseHeaders, endStream: Bool, streamIntel: StreamIntel) + -> FilterHeadersStatus + { + let builder = headers.toResponseHeadersBuilder() + builder.add(name: "filter-demo", value: "1") + return .continue(headers: builder.build()) + } + + func setResponseFilterCallbacks(_ callbacks: ResponseFilterCallbacks, streamIntel: StreamIntel) {} + + func onResponseData(_ body: Data, endStream: Bool, streamIntel: StreamIntel) + -> FilterDataStatus + { + return .continue(data: body) + } + + func onResponseTrailers(_ trailers: ResponseTrailers, streamIntel: StreamIntel) + -> FilterTrailersStatus + { + return .continue(trailers: trailers) + } + + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {} + + func onCancel(streamIntel: FinalStreamIntel) {} + + func onComplete(streamIntel: FinalStreamIntel) {} +} diff --git a/test/swift/apps/experimental/Info.plist b/test/swift/apps/experimental/Info.plist new file mode 100644 index 0000000000..83af5464ce --- /dev/null +++ b/test/swift/apps/experimental/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1 + CFBundleSignature + ???? + CFBundleVersion + 0.1 + ITSAppUsesNonExemptEncryption + + NSAppTransportSecurity + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarStyle + UIStatusBarStyleLightContent + UILaunchStoryboardName + LaunchScreen + + diff --git a/test/swift/apps/experimental/README.md b/test/swift/apps/experimental/README.md new file mode 100644 index 0000000000..53e8270fb7 --- /dev/null +++ b/test/swift/apps/experimental/README.md @@ -0,0 +1 @@ +For instructions on how to use this demo, please head over to our [docs](https://envoy-mobile.github.io/docs/envoy-mobile/latest/start/examples/hello_world.html). diff --git a/test/swift/apps/experimental/ResponseModels.swift b/test/swift/apps/experimental/ResponseModels.swift new file mode 100644 index 0000000000..c83e555320 --- /dev/null +++ b/test/swift/apps/experimental/ResponseModels.swift @@ -0,0 +1,10 @@ +/// Represents a response from the server. +struct Response { + let message: String + let headerMessage: String +} + +/// Error that was encountered when executing a request. +struct RequestError: Error { + let message: String +} diff --git a/test/swift/apps/experimental/ViewController.swift b/test/swift/apps/experimental/ViewController.swift new file mode 100644 index 0000000000..34dc43e322 --- /dev/null +++ b/test/swift/apps/experimental/ViewController.swift @@ -0,0 +1,170 @@ +import Envoy +import UIKit + +private let kCellID = "cell-id" +private let kRequestAuthority = "api.lyft.com" +private let kRequestPath = "/ping" +private let kRequestScheme = "https" +private let kFilteredHeaders = + ["server", "filter-demo", "async-filter-demo", "x-envoy-upstream-service-time"] + +final class ViewController: UITableViewController { + private var results = [Result]() + private var timer: Foundation.Timer? + private var streamClient: StreamClient? + private var pulseClient: PulseClient? + + override func viewDidLoad() { + super.viewDidLoad() + + let engine = EngineBuilder() + .addLogLevel(.debug) + .addPlatformFilter(DemoFilter.init) + .addPlatformFilter(BufferDemoFilter.init) + .addPlatformFilter(AsyncDemoFilter.init) + .enableHappyEyeballs(true) + .enableInterfaceBinding(true) + // swiftlint:disable:next line_length + .addNativeFilter(name: "envoy.filters.http.buffer", typedConfig: "{\"@type\":\"type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer\",\"max_request_bytes\":5242880}") + .setOnEngineRunning { NSLog("Envoy async internal setup completed") } + .addStringAccessor(name: "demo-accessor", accessor: { return "PlatformString" }) + .setEventTracker { NSLog("Envoy event emitted: \($0)") } + .build() + self.streamClient = engine.streamClient() + self.pulseClient = engine.pulseClient() + + NSLog("started Envoy, beginning requests...") + self.startRequests() + } + + deinit { + self.timer?.invalidate() + } + + // MARK: - Requests + + private func startRequests() { + self.timer = .scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + self?.performRequest() + self?.recordStats() + } + } + + private func performRequest() { + guard let streamClient = self.streamClient else { + NSLog("failed to start request - Envoy is not running") + return + } + + NSLog("starting request to '\(kRequestPath)'") + + // Note: this request will use an h2 stream for the upstream request. + // The Objective-C example uses http/1.1. This is done on purpose to test both paths in + // end-to-end tests in CI. + let headers = RequestHeadersBuilder(method: .get, scheme: kRequestScheme, + authority: kRequestAuthority, path: kRequestPath) + .addUpstreamHttpProtocol(.http2) + .build() + + streamClient + .newStreamPrototype() + .setOnResponseHeaders { [weak self] headers, _, _ in + let statusCode = headers.httpStatus.map(String.init) ?? "nil" + let message = "received headers with status \(statusCode)" + + let headerMessage = headers.allHeaders() + .filter { kFilteredHeaders.contains($0.key) } + .map { "\($0.key): \($0.value.joined(separator: ", "))" } + .joined(separator: "\n") + + NSLog(message) + if let filterDemoValue = headers.value(forName: "filter-demo")?.first { + NSLog("filter-demo: \(filterDemoValue)") + } + if let asyncFilterDemoValue = headers.value(forName: "async-filter-demo")?.first { + NSLog("async-filter-demo: \(asyncFilterDemoValue)") + } + + let response = Response(message: message, + headerMessage: headerMessage) + self?.add(result: .success(response)) + } + .setOnError { [weak self] error, _ in + let message: String + if let attemptCount = error.attemptCount { + message = "failed within Envoy library after \(attemptCount) attempts: \(error.message)" + } else { + message = "failed within Envoy library: \(error.message)" + } + + NSLog(message) + self?.add(result: .failure(RequestError(message: message))) + } + .start() + .sendHeaders(headers, endStream: true) + } + + private func add(result: Result) { + self.results.insert(result, at: 0) + self.tableView.reloadData() + } + + private func recordStats() { + guard let pulseClient = self.pulseClient else { + NSLog("failed to send stats - Envoy is not running") + return + } + + let counter = pulseClient.counter(elements: ["foo", "bar", "counter"]) + counter.increment() + counter.increment(count: 5) + + let gauge = pulseClient.gauge(elements: ["foo", "bar", "gauge"]) + gauge.set(value: 5) + gauge.add(amount: 10) + gauge.sub(amount: 1) + + let timer = pulseClient.timer(elements: ["foo", "bar", "timer"]) + let distribution = pulseClient.distribution(elements: ["foo", "bar", "distribution"]) + timer.recordDuration(durationMs: 15) + distribution.recordValue(value: 15) + } + // MARK: - UITableView + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.results.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) + -> UITableViewCell + { + let cell = tableView.dequeueReusableCell(withIdentifier: kCellID) ?? + UITableViewCell(style: .subtitle, reuseIdentifier: kCellID) + + let result = self.results[indexPath.row] + switch result { + case .success(let response): + cell.textLabel?.text = response.message + cell.detailTextLabel?.text = response.headerMessage + + cell.textLabel?.textColor = .black + cell.detailTextLabel?.lineBreakMode = .byWordWrapping + cell.detailTextLabel?.numberOfLines = 0 + cell.detailTextLabel?.textColor = .black + cell.contentView.backgroundColor = .white + case .failure(let error): + cell.textLabel?.text = error.message + cell.detailTextLabel?.text = nil + + cell.textLabel?.textColor = .white + cell.detailTextLabel?.textColor = .white + cell.contentView.backgroundColor = .red + } + + return cell + } +}