Skip to content
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
4 changes: 4 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ lazy val `core-logs` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-laws" % CatsVersion % Test,
"org.typelevel" %%% "discipline-munit" % MUnitDisciplineVersion % Test
),
mimaBinaryIssueFilters ++= Seq(
// LogRecordBuilder is sealed
ProblemFilters.exclude[ReversedMissingMethodProblem]("org.typelevel.otel4s.logs.LogRecordBuilder.withException")
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ sealed trait LogRecordBuilder[F[_], Ctx] {
*/
def withEventName(eventName: String): LogRecordBuilder[F, Ctx]

/** Sets `exception.*` attributes based on the given `Throwable`:
* - `exception.type` is set to the exception class name
* - `exception.message` is set to the exception message
* - `exception.stacktrace` is set to the exception stack trace as a string
*/
def withException(exception: Throwable): LogRecordBuilder[F, Ctx]

/** Adds the given attribute to the builder.
*
* @note
Expand Down Expand Up @@ -162,6 +169,7 @@ object LogRecordBuilder {
def withSeverityText(severityText: String): LogRecordBuilder[F, Ctx] = this
def withBody(body: AnyValue): LogRecordBuilder[F, Ctx] = this
def withEventName(eventName: String): LogRecordBuilder[F, Ctx] = this
def withException(exception: Throwable): LogRecordBuilder[F, Ctx] = this
def addAttribute[A](attribute: Attribute[A]): LogRecordBuilder[F, Ctx] = this
def addAttributes(attributes: Attribute[_]*): LogRecordBuilder[F, Ctx] = this
def addAttributes(attributes: immutable.Iterable[Attribute[_]]): LogRecordBuilder[F, Ctx] = this
Expand Down Expand Up @@ -200,6 +208,9 @@ object LogRecordBuilder {
def withEventName(eventName: String): LogRecordBuilder[G, Ctx] =
builder.withEventName(eventName).liftTo

def withException(exception: Throwable): LogRecordBuilder[G, Ctx] =
builder.withException(exception).liftTo

def addAttribute[A](attribute: Attribute[A]): LogRecordBuilder[G, Ctx] =
builder.addAttribute(attribute).liftTo

Expand Down
28 changes: 2 additions & 26 deletions docs/instrumentation/logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,10 @@ import cats.syntax.all._
import org.typelevel.otel4s.{AnyValue, Attribute, Attributes}
import org.typelevel.otel4s.logs.{LogRecordBuilder, LoggerProvider, Severity}
import org.typelevel.otel4s.logs.{Logger => OtelLogger}
import org.typelevel.otel4s.semconv.attributes.{CodeAttributes, ExceptionAttributes}
import org.typelevel.otel4s.semconv.attributes.CodeAttributes

import scribe._

import java.io.{PrintWriter, StringWriter}

import scala.concurrent.duration._
import scala.util.chaining._

Expand Down Expand Up @@ -162,7 +160,7 @@ final class ScriberLoggerSupport[F[_]: Monad, Ctx](
.collect {
case scribe.throwable.TraceLoggableMessage(throwable) => throwable
}
.foldLeft(builder)((b, t) => b.addAttributes(exceptionAttributes(t)))
.foldLeft(builder)((b, t) => b.withException(t))
}
// context
// MDC
Expand Down Expand Up @@ -193,28 +191,6 @@ final class ScriberLoggerSupport[F[_]: Monad, Ctx](

builder.result()
}

private def exceptionAttributes(exception: Throwable): Attributes = {
val builder = Attributes.newBuilder

builder += ExceptionAttributes.ExceptionType(exception.getClass.getName)

val message = exception.getMessage
if (message != null) {
builder += ExceptionAttributes.ExceptionMessage(message)

}

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

exception.printStackTrace(printWriter)
builder += ExceptionAttributes.ExceptionStacktrace(stringWriter.toString)
}

builder.result()
}

private def dataAttributes(data: Map[String, () => Any]): Attributes = {
val builder = Attributes.newBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ private[oteljava] final case class LogRecordBuilderImpl[F[_]: Sync: AskContext](
def withEventName(eventName: String): LogRecordBuilder[F, Context] =
copy(jBuilder = jBuilder.setEventName(eventName))

def withException(exception: Throwable): LogRecordBuilder[F, Context] =
copy(jBuilder = jBuilder.setException(exception))

def addAttribute[A](attribute: Attribute[A]): LogRecordBuilder[F, Context] =
copy(jBuilder = jBuilder.setAttribute(attribute.key.toJava.asInstanceOf[JAttributeKey[Any]], attribute.value))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.typelevel.otel4s.oteljava.logs

import cats.effect.IO
import io.opentelemetry.api.common.{AttributeKey => JAttributeKey}
import io.opentelemetry.api.common.{Value => JValue}
import io.opentelemetry.api.common.KeyValue
import io.opentelemetry.api.common.ValueType
Expand All @@ -36,6 +37,7 @@ import org.scalacheck.Arbitrary
import org.scalacheck.Gen
import org.scalacheck.effect.PropF
import org.typelevel.otel4s.AnyValue
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.Attributes
import org.typelevel.otel4s.logs.Severity
import org.typelevel.otel4s.logs.scalacheck.Arbitraries._
Expand Down Expand Up @@ -65,14 +67,7 @@ class LogsSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
attributes: Attributes,
) =>
val exporter = InMemoryLogRecordExporter.create()
val sdk = OpenTelemetrySdk
.builder()
.setLoggerProvider(
SdkLoggerProvider.builder().addLogRecordProcessor(SimpleLogRecordProcessor.create(exporter)).build()
)
.build()

val logs = Logs.fromJOpenTelemetry[IO](sdk)
val logs = createLogsModule(exporter)

val loggerName = "test-logger"
val loggerVersion = "1.0.0"
Expand Down Expand Up @@ -127,6 +122,73 @@ class LogsSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
}
}

test("withException emits exception attributes") {
val exporter = InMemoryLogRecordExporter.create()
val logs = createLogsModule(exporter)
val exception = new RuntimeException("error")

for {
logger <- logs.loggerProvider.logger("test-logger").get
_ <- logger.logRecordBuilder.withException(exception).emit
items <- IO.delay(exporter.getFinishedLogRecordItems.asScala.toList)
} yield {
assertEquals(items.size, 1)
val attributes = items.head.getAttributes

assertEquals(
attributes.get(JAttributeKey.stringKey("exception.type")),
classOf[RuntimeException].getCanonicalName
)
assertEquals(
attributes.get(JAttributeKey.stringKey("exception.message")),
"error"
)
assert(
attributes.get(JAttributeKey.stringKey("exception.stacktrace")) != null
)
}
}

test("withException keeps user-supplied exception attributes") {
val exporter = InMemoryLogRecordExporter.create()
val logs = createLogsModule(exporter)

for {
logger <- logs.loggerProvider.logger("test-logger").get
_ <- logger.logRecordBuilder
.addAttribute(Attribute("exception.message", "custom message"))
.withException(new RuntimeException("error"))
.emit
items <- IO.delay(exporter.getFinishedLogRecordItems.asScala.toList)
} yield {
assertEquals(items.size, 1)
val attributes = items.head.getAttributes

assertEquals(
attributes.get(JAttributeKey.stringKey("exception.type")),
classOf[RuntimeException].getCanonicalName
)
assertEquals(
attributes.get(JAttributeKey.stringKey("exception.message")),
"custom message"
)
assert(
attributes.get(JAttributeKey.stringKey("exception.stacktrace")) != null
)
}
}

private def createLogsModule(exporter: InMemoryLogRecordExporter): Logs[IO] = {
val sdk = OpenTelemetrySdk
.builder()
.setLoggerProvider(
SdkLoggerProvider.builder().addLogRecordProcessor(SimpleLogRecordProcessor.create(exporter)).build()
)
.build()

Logs.fromJOpenTelemetry[IO](sdk)
}

private implicit val compareJValue: Compare[JValue[Any], JValue[Any]] =
(left, right) => {
(left.getType, right.getType) match {
Expand Down