Skip to content

Commit

Permalink
Merge pull request #369 from iRevive/sdk-trace/event-data
Browse files Browse the repository at this point in the history
sdk-trace: add `EventData`
  • Loading branch information
iRevive authored Nov 15, 2023
2 parents 98ae9fc + d3f3453 commit fe5cae5
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 2 deletions.
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(): _*)
}

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)
}
}
}

0 comments on commit fe5cae5

Please sign in to comment.