Skip to content

Commit

Permalink
Improve SpanDataProvider#and impl
Browse files Browse the repository at this point in the history
Minimise intermidiate collections created by `AttributeProvider`
and `SpanDataProvider` when combined, and add tests for correct
behaviour when combined.
  • Loading branch information
NthPortal committed Jan 30, 2025
1 parent 0fe9389 commit f9107d2
Show file tree
Hide file tree
Showing 6 changed files with 395 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ package trace.client
import org.typelevel.ci.CIString
import org.typelevel.otel4s.Attributes

import scala.collection.immutable.ArraySeq

/** Provides attributes for spans using requests and responses.
*
* It is RECOMMENDED that callers pass `Request`s and `Response`s that have
Expand Down Expand Up @@ -63,40 +65,55 @@ trait AttributeProvider { self =>
/** @return an `AttributeProvider` that provides the attributes from this and
* another `AttributeProvider`
*/
def and(that: AttributeProvider): AttributeProvider =
new AttributeProvider {
def and(that: AttributeProvider): AttributeProvider = that match {
case AttributeProvider.Multi(providers) =>
AttributeProvider.Multi(this +: providers)
case _ => AttributeProvider.Multi(ArraySeq(this, that))
}
}

object AttributeProvider {

private[client] trait Multi extends AttributeProvider {
protected def providers: Seq[AttributeProvider]
}

private[client] object Multi {
private[this] final class Impl(protected val providers: Seq[AttributeProvider]) extends Multi {
def requestAttributes[F[_]](
request: Request[F],
urlTemplateClassifier: UriTemplateClassifier,
urlRedactor: UriRedactor,
headersAllowedAsAttributes: Set[CIString],
headersAllowedAsAttributes: Set[AuthScheme],
): Attributes =
self.requestAttributes(
request,
urlTemplateClassifier,
urlRedactor,
headersAllowedAsAttributes,
) ++
that.requestAttributes(
providers.foldLeft(Attributes.empty)(
_ ++ _.requestAttributes(
request,
urlTemplateClassifier,
urlRedactor,
headersAllowedAsAttributes,
)

)
def responseAttributes[F[_]](
response: Response[F],
headersAllowedAsAttributes: Set[CIString],
headersAllowedAsAttributes: Set[AuthScheme],
): Attributes =
self.responseAttributes(response, headersAllowedAsAttributes) ++
that.responseAttributes(response, headersAllowedAsAttributes)

providers.foldLeft(Attributes.empty)(
_ ++ _.responseAttributes(response, headersAllowedAsAttributes)
)
def exceptionAttributes(cause: Throwable): Attributes =
self.exceptionAttributes(cause) ++ that.exceptionAttributes(cause)
providers.foldLeft(Attributes.empty)(_ ++ _.exceptionAttributes(cause))

override def and(that: AttributeProvider): AttributeProvider = that match {
case Multi(ps) => new Impl(providers ++ ps)
case _ => new Impl(providers :+ that)
}
}
}

object AttributeProvider {
def apply(providers: Seq[AttributeProvider]): Multi = new Impl(providers)

def unapply(multi: Multi): Some[Seq[AttributeProvider]] = Some(multi.providers)
}

/** Provides an `Attribute` containing this middleware's version. */
val middlewareVersion: AttributeProvider =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import org.typelevel.ci.CIString
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.Attributes

import scala.collection.immutable.ArraySeq

/** Provides a name and attributes for spans using requests and responses.
*
* It is RECOMMENDED that callers pass `Request`s and `Response`s that have
Expand Down Expand Up @@ -93,65 +95,77 @@ trait SpanDataProvider extends AttributeProvider { self =>
headersAllowedAsAttributes,
)

/** Returns an `AttributeProvider` that provides the attributes from this and
* another `AttributeProvider`.
/** Returns a `SpanDataProvider` that provides the attributes from this and
* another [[`AttributeProvider`]].
*
* If `that` is a `SpanAndAttributeProvider`, it will not be used to provide
* If `that` is a `SpanDataProvider`, it will not be used to provide
* span names.
*/
override def and(that: AttributeProvider): SpanDataProvider =
new SpanDataProvider {
type Shared = self.Shared

def processSharedData[F[_]](
request: Request[F],
urlTemplateClassifier: UriTemplateClassifier,
urlRedactor: UriRedactor,
): Shared =
self.processSharedData(request, urlTemplateClassifier, urlRedactor)
override def and(that: AttributeProvider): SpanDataProvider = that match {
case AttributeProvider.Multi(providers) =>
SpanDataProvider.Multi(this, providers)
case _ => SpanDataProvider.Multi(this, ArraySeq(that))
}
}

def spanName[F[_]](
request: Request[F],
urlTemplateClassifier: UriTemplateClassifier,
urlRedactor: UriRedactor,
sharedProcessedData: Shared,
): String =
self.spanName(request, urlTemplateClassifier, urlRedactor, sharedProcessedData)
object SpanDataProvider {

def requestAttributes[F[_]](
request: Request[F],
urlTemplateClassifier: UriTemplateClassifier,
urlRedactor: UriRedactor,
sharedProcessedData: Shared,
headersAllowedAsAttributes: Set[CIString],
): Attributes =
self.requestAttributes(
request,
urlTemplateClassifier,
urlRedactor,
sharedProcessedData,
headersAllowedAsAttributes,
) ++
that.requestAttributes(
// it is crucial that `primary` is never an instance of `Multi`
private final case class Multi(primary: SpanDataProvider, others: Seq[AttributeProvider])
extends SpanDataProvider
with AttributeProvider.Multi {
type Shared = primary.Shared
protected def providers: Seq[AttributeProvider] = primary +: others
def processSharedData[F[_]](
request: Request[F],
urlTemplateClassifier: UriTemplateClassifier,
urlRedactor: UriRedactor,
): Shared = primary.processSharedData(request, urlTemplateClassifier, urlRedactor)
def spanName[F[_]](
request: Request[F],
urlTemplateClassifier: UriTemplateClassifier,
urlRedactor: UriRedactor,
sharedProcessedData: Shared,
): String = primary.spanName(request, urlTemplateClassifier, urlRedactor, sharedProcessedData)
def requestAttributes[F[_]](
request: Request[F],
urlTemplateClassifier: UriTemplateClassifier,
urlRedactor: UriRedactor,
sharedProcessedData: Shared,
headersAllowedAsAttributes: Set[AuthScheme],
): Attributes =
others.foldLeft(
primary
.requestAttributes(
request,
urlTemplateClassifier,
urlRedactor,
sharedProcessedData,
headersAllowedAsAttributes,
)
)(
_ ++ _.requestAttributes(
request,
urlTemplateClassifier,
urlRedactor,
headersAllowedAsAttributes,
)
)
def responseAttributes[F[_]](
response: Response[F],
headersAllowedAsAttributes: Set[AuthScheme],
): Attributes =
others.foldLeft(primary.responseAttributes(response, headersAllowedAsAttributes))(
_ ++ _.responseAttributes(response, headersAllowedAsAttributes)
)
def exceptionAttributes(cause: Throwable): Attributes =
others.foldLeft(primary.exceptionAttributes(cause))(_ ++ _.exceptionAttributes(cause))

def responseAttributes[F[_]](
response: Response[F],
headersAllowedAsAttributes: Set[CIString],
): Attributes =
self.responseAttributes(response, headersAllowedAsAttributes) ++
that.responseAttributes(response, headersAllowedAsAttributes)

def exceptionAttributes(cause: Throwable): Attributes =
self.exceptionAttributes(cause) ++ that.exceptionAttributes(cause)
override def and(that: AttributeProvider): SpanDataProvider = that match {
case AttributeProvider.Multi(providers) => copy(others = others ++ providers)
case _ => copy(others = others :+ that)
}
}

object SpanDataProvider {
}

/** A `SpanAndAttributeProvider` following OpenTelemetry semantic conventions. */
val openTelemetry: SpanDataProvider = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright 2023 http4s.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.http4s
package otel4s.middleware.trace.client

import munit.FunSuite
import munit.Location
import org.typelevel.ci.CIString
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.Attributes

class SpanDataProviderTest extends FunSuite {
import SpanDataProviderTest._

private[this] def check(
output: Attributes,
keys: Seq[String],
last: String,
)(implicit loc: Location): Unit =
assertEquals(
output,
keys.map(k => Attribute(s"test.key.$k", 1L)).to(Attributes) + Attribute("test.last", last),
)

test("and") {
val a = new SimpleAttributeProvider("a")
val b = new SimpleAttributeProvider("b")
val c = new SimpleSpanDataProvider("c")
val d = new SimpleSpanDataProvider("d")

val cabd = c.and(a.and(b).and(d))
assertEquals(cabd.spanName(null, null, null, null.asInstanceOf[cabd.Shared]), "c")
check(cabd.requestAttributes(null, null, null, null), Seq("a", "b", "c", "d"), "d")
check(cabd.responseAttributes(null, null), Seq("a", "b", "c", "d"), "d")
check(cabd.exceptionAttributes(null), Seq("a", "b", "c", "d"), "d")

val db = d.and(b)
assertEquals(db.spanName(null, null, null, null.asInstanceOf[db.Shared]), "d")
check(db.requestAttributes(null, null, null, null), Seq("b", "d"), "b")
check(db.responseAttributes(null, null), Seq("b", "d"), "b")
check(db.exceptionAttributes(null), Seq("b", "d"), "b")
}
}

object SpanDataProviderTest {
private[this] def attr(name: String): Attributes =
Attributes(Attribute(s"test.key.$name", 1L), Attribute("test.last", name))

private final class SimpleAttributeProvider(name: String) extends AttributeProvider {
def requestAttributes[F[_]](
request: Request[F],
urlTemplateClassifier: UriTemplateClassifier,
urlRedactor: UriRedactor,
headersAllowedAsAttributes: Set[CIString],
): Attributes = attr(name)
def responseAttributes[F[_]](
response: Response[F],
headersAllowedAsAttributes: Set[CIString],
): Attributes = attr(name)
def exceptionAttributes(cause: Throwable): Attributes =
attr(name)
}

private final class SimpleSpanDataProvider(name: String) extends SpanDataProvider {
type Shared = Null
def processSharedData[F[_]](
request: Request[F],
urlTemplateClassifier: UriTemplateClassifier,
urlRedactor: UriRedactor,
): Null = null
def spanName[F[_]](
request: Request[F],
urlTemplateClassifier: UriTemplateClassifier,
urlRedactor: UriRedactor,
sharedProcessedData: Null,
): String = name
def requestAttributes[F[_]](
request: Request[F],
urlTemplateClassifier: UriTemplateClassifier,
urlRedactor: UriRedactor,
sharedProcessedData: Null,
headersAllowedAsAttributes: Set[CIString],
): Attributes = attr(name)
def responseAttributes[F[_]](
response: Response[F],
headersAllowedAsAttributes: Set[CIString],
): Attributes = attr(name)
def exceptionAttributes(cause: Throwable): Attributes =
attr(name)
}
}
Loading

0 comments on commit f9107d2

Please sign in to comment.