Skip to content

Commit 98619f2

Browse files
authored
Merge pull request #1190 from iRevive/logs/set-exception
core-logs: add `withException` to the `LogRecordBuilder`
2 parents ac6f7a5 + bca397f commit 98619f2

File tree

5 files changed

+90
-34
lines changed

5 files changed

+90
-34
lines changed

build.sbt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ lazy val `core-logs` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
181181
libraryDependencies ++= Seq(
182182
"org.typelevel" %%% "cats-laws" % CatsVersion % Test,
183183
"org.typelevel" %%% "discipline-munit" % MUnitDisciplineVersion % Test
184+
),
185+
mimaBinaryIssueFilters ++= Seq(
186+
// LogRecordBuilder is sealed
187+
ProblemFilters.exclude[ReversedMissingMethodProblem]("org.typelevel.otel4s.logs.LogRecordBuilder.withException")
184188
)
185189
)
186190

core/logs/src/main/scala/org/typelevel/otel4s/logs/LogRecordBuilder.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ sealed trait LogRecordBuilder[F[_], Ctx] {
114114
*/
115115
def withEventName(eventName: String): LogRecordBuilder[F, Ctx]
116116

117+
/** Sets `exception.*` attributes based on the given `Throwable`:
118+
* - `exception.type` is set to the exception class name
119+
* - `exception.message` is set to the exception message
120+
* - `exception.stacktrace` is set to the exception stack trace as a string
121+
*/
122+
def withException(exception: Throwable): LogRecordBuilder[F, Ctx]
123+
117124
/** Adds the given attribute to the builder.
118125
*
119126
* @note
@@ -162,6 +169,7 @@ object LogRecordBuilder {
162169
def withSeverityText(severityText: String): LogRecordBuilder[F, Ctx] = this
163170
def withBody(body: AnyValue): LogRecordBuilder[F, Ctx] = this
164171
def withEventName(eventName: String): LogRecordBuilder[F, Ctx] = this
172+
def withException(exception: Throwable): LogRecordBuilder[F, Ctx] = this
165173
def addAttribute[A](attribute: Attribute[A]): LogRecordBuilder[F, Ctx] = this
166174
def addAttributes(attributes: Attribute[_]*): LogRecordBuilder[F, Ctx] = this
167175
def addAttributes(attributes: immutable.Iterable[Attribute[_]]): LogRecordBuilder[F, Ctx] = this
@@ -200,6 +208,9 @@ object LogRecordBuilder {
200208
def withEventName(eventName: String): LogRecordBuilder[G, Ctx] =
201209
builder.withEventName(eventName).liftTo
202210

211+
def withException(exception: Throwable): LogRecordBuilder[G, Ctx] =
212+
builder.withException(exception).liftTo
213+
203214
def addAttribute[A](attribute: Attribute[A]): LogRecordBuilder[G, Ctx] =
204215
builder.addAttribute(attribute).liftTo
205216

docs/instrumentation/logs.md

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,10 @@ import cats.syntax.all._
9090
import org.typelevel.otel4s.{AnyValue, Attribute, Attributes}
9191
import org.typelevel.otel4s.logs.{LogRecordBuilder, LoggerProvider, Severity}
9292
import org.typelevel.otel4s.logs.{Logger => OtelLogger}
93-
import org.typelevel.otel4s.semconv.attributes.{CodeAttributes, ExceptionAttributes}
93+
import org.typelevel.otel4s.semconv.attributes.CodeAttributes
9494

9595
import scribe._
9696

97-
import java.io.{PrintWriter, StringWriter}
98-
9997
import scala.concurrent.duration._
10098
import scala.util.chaining._
10199

@@ -162,7 +160,7 @@ final class ScriberLoggerSupport[F[_]: Monad, Ctx](
162160
.collect {
163161
case scribe.throwable.TraceLoggableMessage(throwable) => throwable
164162
}
165-
.foldLeft(builder)((b, t) => b.addAttributes(exceptionAttributes(t)))
163+
.foldLeft(builder)((b, t) => b.withException(t))
166164
}
167165
// context
168166
// MDC
@@ -193,28 +191,6 @@ final class ScriberLoggerSupport[F[_]: Monad, Ctx](
193191

194192
builder.result()
195193
}
196-
197-
private def exceptionAttributes(exception: Throwable): Attributes = {
198-
val builder = Attributes.newBuilder
199-
200-
builder += ExceptionAttributes.ExceptionType(exception.getClass.getName)
201-
202-
val message = exception.getMessage
203-
if (message != null) {
204-
builder += ExceptionAttributes.ExceptionMessage(message)
205-
206-
}
207-
208-
if (exception.getStackTrace.nonEmpty) {
209-
val stringWriter = new StringWriter()
210-
val printWriter = new PrintWriter(stringWriter)
211-
212-
exception.printStackTrace(printWriter)
213-
builder += ExceptionAttributes.ExceptionStacktrace(stringWriter.toString)
214-
}
215-
216-
builder.result()
217-
}
218194

219195
private def dataAttributes(data: Map[String, () => Any]): Attributes = {
220196
val builder = Attributes.newBuilder

oteljava/logs/src/main/scala/org/typelevel/otel4s/oteljava/logs/LogRecordBuilderImpl.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ private[oteljava] final case class LogRecordBuilderImpl[F[_]: Sync: AskContext](
6767
def withEventName(eventName: String): LogRecordBuilder[F, Context] =
6868
copy(jBuilder = jBuilder.setEventName(eventName))
6969

70+
def withException(exception: Throwable): LogRecordBuilder[F, Context] =
71+
copy(jBuilder = jBuilder.setException(exception))
72+
7073
def addAttribute[A](attribute: Attribute[A]): LogRecordBuilder[F, Context] =
7174
copy(jBuilder = jBuilder.setAttribute(attribute.key.toJava.asInstanceOf[JAttributeKey[Any]], attribute.value))
7275

oteljava/logs/src/test/scala/org/typelevel/otel4s/oteljava/logs/LogsSuite.scala

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.typelevel.otel4s.oteljava.logs
1818

1919
import cats.effect.IO
20+
import io.opentelemetry.api.common.{AttributeKey => JAttributeKey}
2021
import io.opentelemetry.api.common.{Value => JValue}
2122
import io.opentelemetry.api.common.KeyValue
2223
import io.opentelemetry.api.common.ValueType
@@ -36,6 +37,7 @@ import org.scalacheck.Arbitrary
3637
import org.scalacheck.Gen
3738
import org.scalacheck.effect.PropF
3839
import org.typelevel.otel4s.AnyValue
40+
import org.typelevel.otel4s.Attribute
3941
import org.typelevel.otel4s.Attributes
4042
import org.typelevel.otel4s.logs.Severity
4143
import org.typelevel.otel4s.logs.scalacheck.Arbitraries._
@@ -65,14 +67,7 @@ class LogsSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
6567
attributes: Attributes,
6668
) =>
6769
val exporter = InMemoryLogRecordExporter.create()
68-
val sdk = OpenTelemetrySdk
69-
.builder()
70-
.setLoggerProvider(
71-
SdkLoggerProvider.builder().addLogRecordProcessor(SimpleLogRecordProcessor.create(exporter)).build()
72-
)
73-
.build()
74-
75-
val logs = Logs.fromJOpenTelemetry[IO](sdk)
70+
val logs = createLogsModule(exporter)
7671

7772
val loggerName = "test-logger"
7873
val loggerVersion = "1.0.0"
@@ -127,6 +122,73 @@ class LogsSuite extends CatsEffectSuite with ScalaCheckEffectSuite {
127122
}
128123
}
129124

125+
test("withException emits exception attributes") {
126+
val exporter = InMemoryLogRecordExporter.create()
127+
val logs = createLogsModule(exporter)
128+
val exception = new RuntimeException("error")
129+
130+
for {
131+
logger <- logs.loggerProvider.logger("test-logger").get
132+
_ <- logger.logRecordBuilder.withException(exception).emit
133+
items <- IO.delay(exporter.getFinishedLogRecordItems.asScala.toList)
134+
} yield {
135+
assertEquals(items.size, 1)
136+
val attributes = items.head.getAttributes
137+
138+
assertEquals(
139+
attributes.get(JAttributeKey.stringKey("exception.type")),
140+
classOf[RuntimeException].getCanonicalName
141+
)
142+
assertEquals(
143+
attributes.get(JAttributeKey.stringKey("exception.message")),
144+
"error"
145+
)
146+
assert(
147+
attributes.get(JAttributeKey.stringKey("exception.stacktrace")) != null
148+
)
149+
}
150+
}
151+
152+
test("withException keeps user-supplied exception attributes") {
153+
val exporter = InMemoryLogRecordExporter.create()
154+
val logs = createLogsModule(exporter)
155+
156+
for {
157+
logger <- logs.loggerProvider.logger("test-logger").get
158+
_ <- logger.logRecordBuilder
159+
.addAttribute(Attribute("exception.message", "custom message"))
160+
.withException(new RuntimeException("error"))
161+
.emit
162+
items <- IO.delay(exporter.getFinishedLogRecordItems.asScala.toList)
163+
} yield {
164+
assertEquals(items.size, 1)
165+
val attributes = items.head.getAttributes
166+
167+
assertEquals(
168+
attributes.get(JAttributeKey.stringKey("exception.type")),
169+
classOf[RuntimeException].getCanonicalName
170+
)
171+
assertEquals(
172+
attributes.get(JAttributeKey.stringKey("exception.message")),
173+
"custom message"
174+
)
175+
assert(
176+
attributes.get(JAttributeKey.stringKey("exception.stacktrace")) != null
177+
)
178+
}
179+
}
180+
181+
private def createLogsModule(exporter: InMemoryLogRecordExporter): Logs[IO] = {
182+
val sdk = OpenTelemetrySdk
183+
.builder()
184+
.setLoggerProvider(
185+
SdkLoggerProvider.builder().addLogRecordProcessor(SimpleLogRecordProcessor.create(exporter)).build()
186+
)
187+
.build()
188+
189+
Logs.fromJOpenTelemetry[IO](sdk)
190+
}
191+
130192
private implicit val compareJValue: Compare[JValue[Any], JValue[Any]] =
131193
(left, right) => {
132194
(left.getType, right.getType) match {

0 commit comments

Comments
 (0)