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

sdk-trace: add EventData #369

Merged
merged 1 commit into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright 2023 Typelevel
*
* 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.typelevel.otel4s.sdk
package trace.data

import cats.Hash
import cats.Show
import cats.syntax.show._
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.semconv.trace.attributes.SemanticAttributes

import java.io.PrintWriter
import java.io.StringWriter
import scala.concurrent.duration.FiniteDuration

/** Data representation of an event.
*
* @see
* [[https://opentelemetry.io/docs/specs/otel/trace/api/#add-events]]
*/
sealed trait EventData {

/** The name of the event.
*/
def name: String

/** The timestamp of the event.
*/
def timestamp: FiniteDuration

/** The attributes of the event.
*/
def attributes: Attributes

override final def hashCode(): Int =
Hash[EventData].hash(this)

override final def equals(obj: Any): Boolean =
obj match {
case other: EventData => Hash[EventData].eqv(this, other)
case _ => false
}

override final def toString: String =
Show[EventData].show(this)
}

object EventData {
private final val ExceptionEventName = "exception"

/** Creates [[EventData]] with the given arguments.
*
* @param name
* the name of the event
*
* @param timestamp
* the timestamp of the event
*
* @param attributes
* the attributes to associate with the event
*/
def apply(
name: String,
timestamp: FiniteDuration,
attributes: Attributes
): EventData =
Impl(name, timestamp, attributes)

/** Creates [[EventData]] from the given exception.
*
* The name of the even will be `exception`.
*
* Exception details (name, message, and stacktrace) will be added to the
* attributes.
*
* @param timestamp
* the timestamp of the event
*
* @param exception
* the exception to associate with the event
*
* @param attributes
* the attributes to associate with the event
*
* @param escaped
* should be set to true if the exception is recorded at a point where it
* is known that the exception is escaping the scope of the span
*/
def fromException(
timestamp: FiniteDuration,
exception: Throwable,
attributes: Attributes,
escaped: Boolean
): EventData = {
val allAttributes = {
val builder = Vector.newBuilder[Attribute[_]]

builder.addOne(
Attribute(SemanticAttributes.ExceptionType, exception.getClass.getName)
)

val message = exception.getMessage
if (message != null) {
builder.addOne(Attribute(SemanticAttributes.ExceptionMessage, message))
}

if (exception.getStackTrace.nonEmpty) {
val stringWriter = new StringWriter()
val printWriter = new PrintWriter(stringWriter)
exception.printStackTrace(printWriter)

builder.addOne(
Attribute(
SemanticAttributes.ExceptionStacktrace,
stringWriter.toString
)
)
}

builder.addOne(Attribute(SemanticAttributes.ExceptionEscaped, escaped))

builder.addAll(attributes.toList)

Attributes(builder.result(): _*)
Comment on lines +136 to +138
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there's a lot of inefficient conversions here: toList, .result(), : _* ...

It can be a follow-up, but I wonder if we can either have a mutable AttributesBuilder that is backed by a MapBuilder or we could have Attributes.unsafeFromMap style builder.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we should explore the alternatives.

}

Impl(ExceptionEventName, timestamp, allAttributes)
}

implicit val eventDataHash: Hash[EventData] =
Hash.by(data => (data.name, data.timestamp, data.attributes))

implicit val eventDataShow: Show[EventData] =
Show.show { data =>
show"EventData{name=${data.name}, timestamp=${data.timestamp}, attributes=${data.attributes}}"
}

private final case class Impl(
name: String,
timestamp: FiniteDuration,
attributes: Attributes
) extends EventData

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ import org.scalacheck.rng.Seed
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.AttributeKey
import org.typelevel.otel4s.AttributeType
import org.typelevel.otel4s.sdk.trace.data.EventData
import org.typelevel.otel4s.sdk.trace.samplers.SamplingDecision
import org.typelevel.otel4s.trace.SpanContext
import org.typelevel.otel4s.trace.TraceFlags
import org.typelevel.otel4s.trace.TraceState

import scala.concurrent.duration.FiniteDuration

object Cogens {

implicit val attributeTypeCogen: Cogen[AttributeType[_]] =
Expand Down Expand Up @@ -92,4 +95,8 @@ object Cogens {
)
}

implicit val eventDataCogen: Cogen[EventData] =
Cogen[(String, FiniteDuration, Attributes)].contramap { data =>
(data.name, data.timestamp, data.attributes)
}
}
12 changes: 10 additions & 2 deletions sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Gens.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.scalacheck.Arbitrary
import org.scalacheck.Gen
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.Attribute.KeySelect
import org.typelevel.otel4s.sdk.trace.data.EventData
import org.typelevel.otel4s.sdk.trace.samplers.SamplingDecision
import org.typelevel.otel4s.trace.SpanContext
import org.typelevel.otel4s.trace.SpanKind
Expand Down Expand Up @@ -108,10 +109,17 @@ object Gens {

val spanContext: Gen[SpanContext] =
for {
traceId <- traceId
spanId <- spanId
traceId <- Gens.traceId
spanId <- Gens.spanId
traceFlags <- Gen.oneOf(TraceFlags.Sampled, TraceFlags.Default)
remote <- Gen.oneOf(true, false)
} yield SpanContext(traceId, spanId, traceFlags, TraceState.empty, remote)

val eventData: Gen[EventData] =
for {
name <- Gen.alphaNumStr
epoch <- Gen.finiteDuration
attributes <- Gens.attributes
} yield EventData(name, epoch, attributes)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2023 Typelevel
*
* 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.typelevel.otel4s.sdk.trace
package data

import cats.Show
import cats.kernel.laws.discipline.HashTests
import cats.syntax.monoid._
import cats.syntax.show._
import munit.DisciplineSuite
import org.scalacheck.Arbitrary
import org.scalacheck.Gen
import org.scalacheck.Prop
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.sdk.Attributes

import java.io.PrintWriter
import java.io.StringWriter
import scala.util.control.NoStackTrace

class EventDataSuite extends DisciplineSuite {
import Cogens.eventDataCogen

private implicit val eventDataArbitrary: Arbitrary[EventData] =
Arbitrary(Gens.eventData)

checkAll("EventData.HashLaws", HashTests[EventData].hash)

test("Show[EventData]") {
Prop.forAll(Gens.eventData) { data =>
val expected =
show"EventData{name=${data.name}, timestamp=${data.timestamp}, attributes=${data.attributes}}"

assertEquals(Show[EventData].show(data), expected)
}
}

test("create EventData with given arguments") {
Prop.forAll(Gens.eventData) { data =>
assertEquals(EventData(data.name, data.timestamp, data.attributes), data)
}
}

test("create EventData from an exception") {
Prop.forAll(Gen.finiteDuration, Gens.attributes) { (ts, attributes) =>
val exception = new RuntimeException("This is fine")

val stringWriter = new StringWriter()
val printWriter = new PrintWriter(stringWriter)
exception.printStackTrace(printWriter)

val expectedAttributes = Attributes(
Attribute("exception.type", exception.getClass.getName),
Attribute("exception.message", exception.getMessage),
Attribute("exception.stacktrace", stringWriter.toString),
Attribute("exception.escaped", true)
) |+| attributes

val data = EventData.fromException(ts, exception, attributes, true)

assertEquals(data.name, "exception")
assertEquals(data.timestamp, ts)
assertEquals(data.attributes, expectedAttributes)
}
}

test("create EventData from an exception (no message, no stack trace)") {
Prop.forAll(Gen.finiteDuration, Gens.attributes) { (ts, attributes) =>
val exception = new RuntimeException with NoStackTrace

val stringWriter = new StringWriter()
val printWriter = new PrintWriter(stringWriter)
exception.printStackTrace(printWriter)

val expectedAttributes = Attributes(
Attribute("exception.type", exception.getClass.getName),
Attribute("exception.escaped", false)
) |+| attributes

val data = EventData.fromException(ts, exception, attributes, false)

assertEquals(data.name, "exception")
assertEquals(data.timestamp, ts)
assertEquals(data.attributes, expectedAttributes)
}
}
}