elastic / apm-agent-android

Elastic APM Android Agent
Apache License 2.0
20 stars 6 forks source link

Set Authorization header dynamically #347

Open almozavr opened 4 weeks ago

almozavr commented 4 weeks ago

Context

Connectivity is used to provide custom authorization header value. It's used once the whole apm agent is being initialized.

Issue

The whole APM service could live behind a custom auth proxy server, and authorization token could change dynamically. Currently there is no way to change the auth token value without resetting and re-initializing the whole agent.

Possible solution

Let Connectivity return "provider" for authConfiguration() and use setHeaders(Supplier<Map<String, String>> headerSupplier) instead of addHeader(...) for exporters.

Even better would be to clone setHeader behaviour for Connectivity to return a set of headers for every request instead of single auth header value -> because e.g. proxy server could demand some custom headers to identify user – this would be the most flexible yet easy implementable for current key/secret predefined variants.

LikeTheSalad commented 3 weeks ago

Thank you for your feedback. It makes sense, I'm currently pondering whether this could be an addition upstream or it should instead be added to this agent. In either case, it's definitely a feature that will get added soon. In the meantime, a workaround could be providing your own custom exporters via SignalConfiguration.custom into the ElasticApmConfiguration object where each exporter can be a wrapper that delegates to a new one once the auth params change.

almozavr commented 2 weeks ago

Thanks! I did exactly this as a tmp solution and can confirm it works. Though a more flexible out-of-the-box headers solution would be way better than this

class DynamicHeadersSignalConfiguration(
    private val connectivity: ConnectivityConfiguration
) : DefaultSignalProcessorConfiguration() {

    override fun provideSpanExporter(): SpanExporter {
        return when (connectivity.exportProtocol) {
            ExportProtocol.GRPC -> otlpGrpcSpanExporter
            ExportProtocol.HTTP -> otlpHttpSpanExporter
            else -> throw IllegalArgumentException()
        }
    }

    override fun provideLogExporter(): LogRecordExporter {
        return when (connectivity.exportProtocol) {
            ExportProtocol.GRPC -> otlpGrpcLogRecordExporter
            ExportProtocol.HTTP -> otlpHttpLogRecordExporter
            else -> throw IllegalArgumentException()
        }
    }

    override fun provideMetricExporter(): MetricExporter {
        return when (connectivity.exportProtocol) {
            ExportProtocol.GRPC -> otlpGrpcMetricExporter
            ExportProtocol.HTTP -> otlpHttpMetricExporter
            else -> throw IllegalArgumentException()
        }
    }

    private val otlpGrpcSpanExporter: OtlpGrpcSpanExporter
        get() {
            val exporterBuilder =
                OtlpGrpcSpanExporter.builder()
                    .setEndpoint(connectivity.endpoint)
                    .setHeaders {
                        buildMap {
                            putAuthHeader()
                        }
                    }
            return exporterBuilder.build()
        }

    private val otlpGrpcLogRecordExporter: OtlpGrpcLogRecordExporter
        get() {
            val exporterBuilder =
                OtlpGrpcLogRecordExporter.builder()
                    .setEndpoint(connectivity.endpoint)
                    .setHeaders {
                        buildMap {
                            putAuthHeader()
                        }
                    }
            return exporterBuilder.build()
        }

    private val otlpGrpcMetricExporter: OtlpGrpcMetricExporter
        get() {
            val exporterBuilder = OtlpGrpcMetricExporter.builder()
                .setAggregationTemporalitySelector(AggregationTemporalitySelector.deltaPreferred())
                .setEndpoint(connectivity.endpoint)
                .setHeaders {
                    buildMap {
                        connectivity.authConfiguration.asAuthorizationHeaderValue()?.takeIf { it.isNotEmpty() }?.also {
                            put(AUTHORIZATION_HEADER_NAME, it)
                        }
                    }
                }
            return exporterBuilder.build()
        }

    private val otlpHttpSpanExporter: OtlpHttpSpanExporter
        get() {
            val exporterBuilder =
                OtlpHttpSpanExporter.builder()
                    .setEndpoint(getHttpEndpoint("traces"))
                    .setHeaders {
                        buildMap {
                            putAuthHeader()
                        }
                    }
            return exporterBuilder.build()
        }

    private val otlpHttpLogRecordExporter: OtlpHttpLogRecordExporter
        get() {
            val exporterBuilder =
                OtlpHttpLogRecordExporter.builder()
                    .setEndpoint(getHttpEndpoint("logs"))
                    .setHeaders {
                        buildMap {
                            putAuthHeader()
                        }
                    }
            return exporterBuilder.build()
        }

    private val otlpHttpMetricExporter: OtlpHttpMetricExporter
        get() {
            val exporterBuilder = OtlpHttpMetricExporter.builder()
                .setAggregationTemporalitySelector(AggregationTemporalitySelector.deltaPreferred())
                .setEndpoint(getHttpEndpoint("metrics"))
                .setHeaders {
                    buildMap {
                        putAuthHeader()
                    }
                }
            return exporterBuilder.build()
        }

    private fun getHttpEndpoint(signalId: String): String {
        return String.format("%s/v1/%s", connectivity.endpoint, signalId)
    }

    private fun MutableMap<String, String>.putAuthHeader() {
        connectivity.authConfiguration?.asAuthorizationHeaderValue()?.takeIf { it.isNotEmpty() }?.also {
            put(AUTHORIZATION_HEADER_NAME, it)
        }
    }

    companion object {

        private const val AUTHORIZATION_HEADER_NAME = "Authorization"
    }
}
LikeTheSalad commented 2 weeks ago

Glad to know it worked. I'm currently proposing this change upstream, I'll come back here once there's a response from the community. If they deny it I'll add the option to the Elastic agent only.

almozavr commented 2 weeks ago

In this context, it would be also great to have a stop, flush & cleanup functionality: e.g. if it's critical to cleanup any session related data once user logs out. Currently, there is ElasticApmAgent.resetForTest() but it's name is suspicious and I'm not sure if it removes everything tracked from the memory.