Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set Authorization header dynamically #347

Open
almozavr opened this issue Aug 16, 2024 · 4 comments
Open

Set Authorization header dynamically #347

almozavr opened this issue Aug 16, 2024 · 4 comments

Comments

@almozavr
Copy link

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.

@almozavr almozavr changed the title [Java/Android] Set Authorization header dynamically Set Authorization header dynamically Aug 16, 2024
@LikeTheSalad LikeTheSalad self-assigned this Aug 19, 2024
@LikeTheSalad
Copy link
Contributor

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
Copy link
Author

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
Copy link
Contributor

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
Copy link
Author

almozavr commented Aug 28, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants