Skip to content

Commit

Permalink
core-trace-experimental: add @withSpan macro
Browse files Browse the repository at this point in the history
  • Loading branch information
iRevive committed May 10, 2024
1 parent 9a61034 commit 9b10452
Show file tree
Hide file tree
Showing 8 changed files with 932 additions and 2 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p semconv/stable/.jvm/target oteljava/metrics/target sdk-exporter/common/.js/target sdk/common/.native/target sdk/common/.js/target core/trace/.js/target sdk-exporter/all/.jvm/target semconv/experimental/.js/target sdk/trace/.js/target core/common/.jvm/target sdk-exporter/common/.native/target oteljava/common-testkit/target sdk/metrics/.native/target sdk-exporter/metrics/.jvm/target sdk-exporter/trace/.jvm/target unidocs/target sdk-exporter/metrics/.native/target oteljava/trace-testkit/target core/metrics/.native/target core/all/.native/target sdk/trace-testkit/.jvm/target sdk/trace-testkit/.native/target sdk/testkit/.native/target semconv/experimental/.native/target core/metrics/.jvm/target core/all/.js/target sdk-exporter/proto/.jvm/target sdk-exporter/proto/.js/target sdk-exporter/metrics/.js/target semconv/stable/.native/target sdk/all/.native/target sdk/metrics-testkit/.js/target core/metrics/.js/target sdk/testkit/.js/target core/all/.jvm/target sdk-exporter/trace/.native/target sdk/common/.jvm/target core/trace/.native/target oteljava/metrics-testkit/target sdk/trace/.native/target semconv/experimental/.jvm/target sdk/metrics-testkit/.native/target sdk/metrics/.jvm/target oteljava/common/target scalafix/rules/target sdk-exporter/proto/.native/target core/trace/.jvm/target sdk-exporter/common/.jvm/target sdk/metrics-testkit/.jvm/target sdk/metrics/.js/target sdk-exporter/trace/.js/target core/common/.native/target sdk/trace-testkit/.js/target core/common/.js/target oteljava/trace/target oteljava/testkit/target sdk/testkit/.jvm/target sdk-exporter/all/.js/target sdk/all/.js/target sdk/all/.jvm/target sdk-exporter/all/.native/target oteljava/all/target sdk/trace/.jvm/target semconv/stable/.js/target project/target
run: mkdir -p semconv/stable/.jvm/target oteljava/metrics/target core/trace-experimental/.js/target sdk-exporter/common/.js/target sdk/common/.native/target sdk/common/.js/target core/trace/.js/target sdk-exporter/all/.jvm/target semconv/experimental/.js/target sdk/trace/.js/target core/common/.jvm/target sdk-exporter/common/.native/target oteljava/common-testkit/target sdk/metrics/.native/target sdk-exporter/metrics/.jvm/target sdk-exporter/trace/.jvm/target unidocs/target sdk-exporter/metrics/.native/target oteljava/trace-testkit/target core/metrics/.native/target core/all/.native/target sdk/trace-testkit/.jvm/target sdk/trace-testkit/.native/target sdk/testkit/.native/target semconv/experimental/.native/target core/metrics/.jvm/target core/all/.js/target sdk-exporter/proto/.jvm/target sdk-exporter/proto/.js/target sdk-exporter/metrics/.js/target semconv/stable/.native/target sdk/all/.native/target sdk/metrics-testkit/.js/target core/trace-experimental/.native/target core/metrics/.js/target sdk/testkit/.js/target core/all/.jvm/target sdk-exporter/trace/.native/target sdk/common/.jvm/target core/trace-experimental/.jvm/target core/trace/.native/target oteljava/metrics-testkit/target sdk/trace/.native/target semconv/experimental/.jvm/target sdk/metrics-testkit/.native/target sdk/metrics/.jvm/target oteljava/common/target scalafix/rules/target sdk-exporter/proto/.native/target core/trace/.jvm/target sdk-exporter/common/.jvm/target sdk/metrics-testkit/.jvm/target sdk/metrics/.js/target sdk-exporter/trace/.js/target core/common/.native/target sdk/trace-testkit/.js/target core/common/.js/target oteljava/trace/target oteljava/testkit/target sdk/testkit/.jvm/target sdk-exporter/all/.js/target sdk/all/.js/target sdk/all/.jvm/target sdk-exporter/all/.native/target oteljava/all/target sdk/trace/.jvm/target semconv/stable/.js/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar semconv/stable/.jvm/target oteljava/metrics/target sdk-exporter/common/.js/target sdk/common/.native/target sdk/common/.js/target core/trace/.js/target sdk-exporter/all/.jvm/target semconv/experimental/.js/target sdk/trace/.js/target core/common/.jvm/target sdk-exporter/common/.native/target oteljava/common-testkit/target sdk/metrics/.native/target sdk-exporter/metrics/.jvm/target sdk-exporter/trace/.jvm/target unidocs/target sdk-exporter/metrics/.native/target oteljava/trace-testkit/target core/metrics/.native/target core/all/.native/target sdk/trace-testkit/.jvm/target sdk/trace-testkit/.native/target sdk/testkit/.native/target semconv/experimental/.native/target core/metrics/.jvm/target core/all/.js/target sdk-exporter/proto/.jvm/target sdk-exporter/proto/.js/target sdk-exporter/metrics/.js/target semconv/stable/.native/target sdk/all/.native/target sdk/metrics-testkit/.js/target core/metrics/.js/target sdk/testkit/.js/target core/all/.jvm/target sdk-exporter/trace/.native/target sdk/common/.jvm/target core/trace/.native/target oteljava/metrics-testkit/target sdk/trace/.native/target semconv/experimental/.jvm/target sdk/metrics-testkit/.native/target sdk/metrics/.jvm/target oteljava/common/target scalafix/rules/target sdk-exporter/proto/.native/target core/trace/.jvm/target sdk-exporter/common/.jvm/target sdk/metrics-testkit/.jvm/target sdk/metrics/.js/target sdk-exporter/trace/.js/target core/common/.native/target sdk/trace-testkit/.js/target core/common/.js/target oteljava/trace/target oteljava/testkit/target sdk/testkit/.jvm/target sdk-exporter/all/.js/target sdk/all/.js/target sdk/all/.jvm/target sdk-exporter/all/.native/target oteljava/all/target sdk/trace/.jvm/target semconv/stable/.js/target project/target
run: tar cf targets.tar semconv/stable/.jvm/target oteljava/metrics/target core/trace-experimental/.js/target sdk-exporter/common/.js/target sdk/common/.native/target sdk/common/.js/target core/trace/.js/target sdk-exporter/all/.jvm/target semconv/experimental/.js/target sdk/trace/.js/target core/common/.jvm/target sdk-exporter/common/.native/target oteljava/common-testkit/target sdk/metrics/.native/target sdk-exporter/metrics/.jvm/target sdk-exporter/trace/.jvm/target unidocs/target sdk-exporter/metrics/.native/target oteljava/trace-testkit/target core/metrics/.native/target core/all/.native/target sdk/trace-testkit/.jvm/target sdk/trace-testkit/.native/target sdk/testkit/.native/target semconv/experimental/.native/target core/metrics/.jvm/target core/all/.js/target sdk-exporter/proto/.jvm/target sdk-exporter/proto/.js/target sdk-exporter/metrics/.js/target semconv/stable/.native/target sdk/all/.native/target sdk/metrics-testkit/.js/target core/trace-experimental/.native/target core/metrics/.js/target sdk/testkit/.js/target core/all/.jvm/target sdk-exporter/trace/.native/target sdk/common/.jvm/target core/trace-experimental/.jvm/target core/trace/.native/target oteljava/metrics-testkit/target sdk/trace/.native/target semconv/experimental/.jvm/target sdk/metrics-testkit/.native/target sdk/metrics/.jvm/target oteljava/common/target scalafix/rules/target sdk-exporter/proto/.native/target core/trace/.jvm/target sdk-exporter/common/.jvm/target sdk/metrics-testkit/.jvm/target sdk/metrics/.js/target sdk-exporter/trace/.js/target core/common/.native/target sdk/trace-testkit/.js/target core/common/.js/target oteljava/trace/target oteljava/testkit/target sdk/testkit/.jvm/target sdk-exporter/all/.js/target sdk/all/.js/target sdk/all/.jvm/target sdk-exporter/all/.native/target oteljava/all/target sdk/trace/.jvm/target semconv/stable/.js/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down
17 changes: 17 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ lazy val root = tlCrossRootProject
`core-common`,
`core-metrics`,
`core-trace`,
`core-trace-experimental`,
core,
`sdk-common`,
`sdk-metrics`,
Expand Down Expand Up @@ -186,6 +187,21 @@ lazy val `core-trace` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
)
.settings(scalafixSettings)

lazy val `core-trace-experimental` =
crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("core/trace-experimental"))
.dependsOn(`core-trace`)
.settings(scalaReflectDependency)
.settings(munitDependencies)
.settings(
name := "otel4s-core-trace-experimental",
scalacOptions ++= {
if (tlIsScala3.value) Nil else Seq("-Ymacro-annotations")
},
)
.settings(scalafixSettings)

lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("core/all"))
Expand Down Expand Up @@ -691,6 +707,7 @@ lazy val unidocs = project
`core-common`.jvm,
`core-metrics`.jvm,
`core-trace`.jvm,
`core-trace-experimental`.jvm,
core.jvm,
`sdk-common`.jvm,
`sdk-metrics`.jvm,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/*
* Copyright 2022 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.trace.experimental

import scala.annotation.StaticAnnotation
import scala.annotation.compileTimeOnly
import scala.annotation.unused
import scala.reflect.macros.blackbox

/** Wraps the body of an annotated method or variable into the span.
*
* By default, the span name will be `className.methodName`, unless a name is
* provided as an argument.
*
* {{{
* class Service[F[_]: Tracer](db: Database[F]) {
* @withSpan
* def findUser(@spanAttribute userId: Long): F[User] =
* db.findUser(id)
* }
*
* // expands into
*
* class Service[F[_]: Tracer](db: Database[F]) {
* def findUser(userId: Long): F[User] =
* Tracer[F]
* .span("Service.findUser", Attribute("userId", userId))
* .surround(db.findUser(id))
* }
* }}}
*
* @param name
* the custom name of the span. If not specified, the span name will be
* `className.methodName`
*
* @param debug
* whether to print the generated code to the console
*/
@compileTimeOnly("enable macro to expand macro annotations")
class withSpan(
@unused name: String = "",
@unused debug: Boolean = false
) extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro withSpanMacro.impl
}

object withSpanMacro {

def impl(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[c.Tree] = {
import c.universe._

def abort(message: String) =
c.abort(c.enclosingPosition, s"@withSpan macro: $message")

def ensureImplicitExist(tpe: Tree, reason: Throwable => String): c.Tree =
try {
val t = c.typecheck(tpe).tpe
c.inferImplicitValue(t)
} catch {
case e: Throwable => abort(reason(e))
}

def resolveEffectType(tpt: Tree): Tree =
tpt match {
case tq"$tpe[${_}]" => tpe
case _ => abort("cannot resolve the type of the effect")
}

val macroName: Tree =
c.prefix.tree match {
case Apply(Select(New(name), _), _) => name
case _ => c.abort(c.enclosingPosition, "Unexpected macro application")
}

val (nameParam, debug) = c.prefix.tree match {
case q"new ${`macroName`}(..$args)" =>
(
args.headOption
.collect { case Literal(value) => Left(value) }
.orElse(
args.collectFirst { case q"name = $value" => Right(value) }
),
args.collectFirst { case q"debug = true" => }.isDefined
)
case _ =>
(None, false)
}

def spanName(definition: ValOrDefDef): Tree =
nameParam match {
case Some(Left(const)) =>
val literal = Literal(const)
q"$literal"

case Some(Right(tree)) =>
tree

case None =>
@annotation.tailrec
def resolveEnclosingName(symbol: Symbol, output: String): String =
if (symbol.isClass) {
val className = symbol.name.toString
if (className.startsWith("$anon"))
resolveEnclosingName(symbol.owner, "$anon" + "." + output)
else
className + "." + output
} else {
resolveEnclosingName(symbol.owner, output)
}

val prefix = resolveEnclosingName(c.internal.enclosingOwner, "")

val literal = Literal(
Constant(prefix + definition.name.decodedName.toString)
)
q"$literal"
}

def expandDef(defDef: DefDef): Tree = {
val effectType = resolveEffectType(defDef.tpt)
val name: Tree = spanName(defDef)

val attributes = defDef.vparamss.flatten.flatMap {
case ValDef(mods, name, tpt, _) =>
mods.annotations.flatMap { annotation =>
val typed = c.typecheck(annotation)
if (
typed.tpe.typeSymbol.fullName == "org.typelevel.otel4s.trace.experimental.spanAttribute"
) {
val keyArg = typed match {
case q"new ${_}(${keyArg})" =>
keyArg
case _ =>
abort("unknown structure of the @spanAttribute annotation.")
}

val key = keyArg match {
// the key param is not specified, use param name as a key
case Select(_, TermName("$lessinit$greater$default$1")) =>
Literal(Constant(name.decodedName.toString))

// type of the AttributeKey must match the parameter type
case q"$_[$tpe]($_)(..$_)" =>
val keyType = c.untypecheck(tpe)
val argType = c.typecheck(tpt, c.TYPEmode)

if (!keyType.equalsStructure(argType)) {
abort(
s"the argument [${name.toString}] type [$argType] does not match the type of the attribute [$keyType]."
)
}
keyArg

case _ =>
keyArg
}

ensureImplicitExist(
q"_root_.org.typelevel.otel4s.AttributeKey.KeySelect[$tpt]",
e =>
s"the argument [${name.decodedName}] cannot be used as an attribute. The type [$tpt] is not supported.${e.getMessage}"
)

List(
q"_root_.org.typelevel.otel4s.Attribute($key, $name)"
)
} else {
Nil
}
}

case _ =>
Nil
}

val body =
q"""
_root_.org.typelevel.otel4s.trace.Tracer[$effectType].span($name, ..$attributes).surround {
${defDef.rhs}
}
"""

DefDef(
defDef.mods,
defDef.name,
defDef.tparams,
defDef.vparamss,
defDef.tpt,
body
)
}

def expandVal(valDef: ValDef): Tree = {
val effectType = resolveEffectType(valDef.tpt)
val name: Tree = spanName(valDef)

val body =
q"""
_root_.org.typelevel.otel4s.trace.Tracer[$effectType].span($name).surround {
${valDef.rhs}
}
"""

ValDef(valDef.mods, valDef.name, valDef.tpt, body)
}

val result = annottees.map(_.tree).toList match {
case List(defDef: DefDef) =>
expandDef(defDef)

case List(valDef: ValDef) =>
expandVal(valDef)

case _ =>
abort(
"unsupported definition. Only `def` and `val` with explicit result types and defined bodies are supported."
)
}

if (debug) {
val at: String =
if (c.enclosingPosition == NoPosition)
""
else
s"at ${c.enclosingPosition.source.file.name}:${c.enclosingPosition.line} "

scala.Predef.println(s"@withSpan $at- expanded into:\n${result}")
}

c.Expr(result)
}

}

0 comments on commit 9b10452

Please sign in to comment.