From 6f5b2988e0aa3b74f0f7bae02f76508582b66455 Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Sat, 30 Nov 2024 13:22:47 +0200 Subject: [PATCH] sdk-exporter: generate proto models in the private package --- .scalafix.conf | 4 ++ build.sbt | 45 ++++++++++++++++++- .../otel4s/sdk/exporter/otlp/OtlpClient.scala | 2 +- .../sdk/exporter/otlp/ProtoEncoder.scala | 10 ++--- .../sdk/exporter/otlp/ProtoEncoderSuite.scala | 2 +- .../otlp/metrics/MetricsProtoEncoder.scala | 8 ++-- .../proto/src/main/protobuf/package.proto | 43 ++++++++++++++++++ .../META-INF/services/scalafix.v1.Rule | 1 + .../scalafix/scala/fix/NoPublicClasses.scala | 30 +++++++++++++ .../otlp/trace/SpansProtoEncoder.scala | 10 ++--- 10 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 sdk-exporter/proto/src/main/protobuf/package.proto create mode 100644 sdk-exporter/proto/src/scalafix/resources/META-INF/services/scalafix.v1.Rule create mode 100644 sdk-exporter/proto/src/scalafix/scala/fix/NoPublicClasses.scala diff --git a/.scalafix.conf b/.scalafix.conf index 1fe4540f7..f8b633001 100644 --- a/.scalafix.conf +++ b/.scalafix.conf @@ -15,3 +15,7 @@ OrganizeImports { importSelectorsOrder = SymbolsFirst removeUnused = false } + +triggered.rules = [ + NoPublicClasses +] diff --git a/build.sbt b/build.sbt index a0a21f021..2994b2012 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,6 @@ import com.typesafe.tools.mima.core._ -ThisBuild / tlBaseVersion := "0.11" +ThisBuild / tlBaseVersion := "0.12" ThisBuild / organization := "org.typelevel" ThisBuild / organizationName := "Typelevel" @@ -376,6 +376,28 @@ lazy val `sdk-exporter-proto` = Compile / PB.targets ++= Seq( scalapb.gen(grpc = false) -> (Compile / sourceManaged).value / "scalapb" ), + Compile / PB.generate := { + val files = (Compile / PB.generate).value + + files.filter(_.isFile).foreach { file => + val content = IO.read(file) + + // see: https://github.com/scalapb/ScalaPB/issues/1778 + val updated = content + .replaceAll( + """(?m)^object (\w+) extends _root_\.scalapb\.GeneratedEnumCompanion\[\w+\]""", + "private[exporter] object $1 extends _root_.scalapb.GeneratedEnumCompanion[$1]" + ) + .replaceAll( + """(?m)^object (\w+) extends _root_\.scalapb\.GeneratedFileObject""", + "private[exporter] object $1 extends _root_.scalapb.GeneratedFileObject" + ) + + IO.write(file, updated) + } + + files + }, scalacOptions := { val opts = scalacOptions.value if (tlIsScala3.value) opts.filterNot(_ == "-Wvalue-discard") else opts @@ -383,9 +405,30 @@ lazy val `sdk-exporter-proto` = // We use open-telemetry protobuf spec to generate models // See https://scalapb.github.io/docs/third-party-protos/#there-is-a-library-on-maven-with-the-protos-and-possibly-generated-java-code libraryDependencies ++= Seq( + "com.thesamet.scalapb" %% "scalapb-runtime" % scalapb.compiler.Version.scalapbVersion % "protobuf", "io.opentelemetry.proto" % "opentelemetry-proto" % OpenTelemetryProtoVersion % "protobuf-src" intransitive () ) ) + .jvmSettings( + // scalafix settings to ensure there are no public classes in the module + // run scalafix against generated sources + Compile / ScalafixPlugin.autoImport.scalafix / unmanagedSources := (Compile / managedSources).value, + // run scalafix only on scala 2.13 + scalafixOnCompile := !tlIsScala3.value, + // read scalafix rules from a shared folder + ScalafixConfig / sourceDirectory := { + if (tlIsScala3.value) { + (ScalafixConfig / sourceDirectory).value + } else { + baseDirectory.value.getParentFile / "src" / "scalafix" + } + }, + // required by scalafix rules + libraryDependencies ++= { + if (tlIsScala3.value) Nil + else Seq("ch.epfl.scala" %% "scalafix-core" % _root_.scalafix.sbt.BuildInfo.scalafixVersion % ScalafixConfig) + } + ) .settings(scalafixSettings) lazy val `sdk-exporter-common` = diff --git a/sdk-exporter/common/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/OtlpClient.scala b/sdk-exporter/common/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/OtlpClient.scala index f5f032719..ecd83c515 100644 --- a/sdk-exporter/common/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/OtlpClient.scala +++ b/sdk-exporter/common/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/OtlpClient.scala @@ -32,7 +32,6 @@ import fs2.Stream import fs2.compression.Compression import fs2.io.net.Network import fs2.io.net.tls.TLSContext -import io.opentelemetry.proto.collector.trace.v1.trace_service.ExportTraceServiceResponse import org.http4s.ContentCoding import org.http4s.EntityEncoder import org.http4s.Header @@ -57,6 +56,7 @@ import org.typelevel.otel4s.sdk.exporter.RetryPolicy import org.typelevel.otel4s.sdk.exporter.otlp.grpc.GrpcCodecs import org.typelevel.otel4s.sdk.exporter.otlp.grpc.GrpcHeaders import org.typelevel.otel4s.sdk.exporter.otlp.grpc.GrpcStatusException +import org.typelevel.otel4s.sdk.exporter.proto.trace_service.ExportTraceServiceResponse import scalapb_circe.Printer import scodec.Attempt import scodec.DecodeResult diff --git a/sdk-exporter/common/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/ProtoEncoder.scala b/sdk-exporter/common/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/ProtoEncoder.scala index 0d79fc677..f910e3bea 100644 --- a/sdk-exporter/common/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/ProtoEncoder.scala +++ b/sdk-exporter/common/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/ProtoEncoder.scala @@ -19,12 +19,12 @@ package sdk package exporter.otlp import io.circe.Json -import io.opentelemetry.proto.common.v1.common.{InstrumentationScope => ScopeProto} -import io.opentelemetry.proto.common.v1.common.AnyValue -import io.opentelemetry.proto.common.v1.common.ArrayValue -import io.opentelemetry.proto.common.v1.common.KeyValue -import io.opentelemetry.proto.resource.v1.resource.{Resource => ResourceProto} import org.typelevel.otel4s.sdk.common.InstrumentationScope +import org.typelevel.otel4s.sdk.exporter.proto.common.{InstrumentationScope => ScopeProto} +import org.typelevel.otel4s.sdk.exporter.proto.common.AnyValue +import org.typelevel.otel4s.sdk.exporter.proto.common.ArrayValue +import org.typelevel.otel4s.sdk.exporter.proto.common.KeyValue +import org.typelevel.otel4s.sdk.exporter.proto.resource.{Resource => ResourceProto} import scalapb.GeneratedMessage import scalapb_circe.Printer diff --git a/sdk-exporter/common/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/ProtoEncoderSuite.scala b/sdk-exporter/common/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/ProtoEncoderSuite.scala index c6bb95bf1..9c307de57 100644 --- a/sdk-exporter/common/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/ProtoEncoderSuite.scala +++ b/sdk-exporter/common/src/test/scala/org/typelevel/otel4s/sdk/exporter/otlp/ProtoEncoderSuite.scala @@ -19,7 +19,6 @@ package org.typelevel.otel4s.sdk.exporter.otlp import io.circe.Encoder import io.circe.Json import io.circe.syntax._ -import io.opentelemetry.proto.common.v1.common.KeyValue import munit.ScalaCheckSuite import org.scalacheck.Arbitrary import org.scalacheck.Prop @@ -29,6 +28,7 @@ import org.typelevel.otel4s.AttributeType import org.typelevel.otel4s.Attributes import org.typelevel.otel4s.sdk.TelemetryResource import org.typelevel.otel4s.sdk.common.InstrumentationScope +import org.typelevel.otel4s.sdk.exporter.proto.common.KeyValue import org.typelevel.otel4s.sdk.scalacheck.Arbitraries._ import scalapb_circe.Printer diff --git a/sdk-exporter/metrics/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/metrics/MetricsProtoEncoder.scala b/sdk-exporter/metrics/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/metrics/MetricsProtoEncoder.scala index 0fc8a242d..c077d9ca5 100644 --- a/sdk-exporter/metrics/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/metrics/MetricsProtoEncoder.scala +++ b/sdk-exporter/metrics/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/metrics/MetricsProtoEncoder.scala @@ -20,11 +20,11 @@ package exporter.otlp.metrics import com.google.protobuf.ByteString import io.circe.Json -import io.opentelemetry.proto.collector.metrics.v1.metrics_service.ExportMetricsServiceRequest -import io.opentelemetry.proto.metrics.v1.{metrics => Proto} -import io.opentelemetry.proto.metrics.v1.metrics.ResourceMetrics -import io.opentelemetry.proto.metrics.v1.metrics.ScopeMetrics import org.typelevel.otel4s.sdk.exporter.otlp.ProtoEncoder +import org.typelevel.otel4s.sdk.exporter.proto.{metrics => Proto} +import org.typelevel.otel4s.sdk.exporter.proto.metrics.ResourceMetrics +import org.typelevel.otel4s.sdk.exporter.proto.metrics.ScopeMetrics +import org.typelevel.otel4s.sdk.exporter.proto.metrics_service.ExportMetricsServiceRequest import org.typelevel.otel4s.sdk.metrics.data.AggregationTemporality import org.typelevel.otel4s.sdk.metrics.data.ExemplarData import org.typelevel.otel4s.sdk.metrics.data.MetricData diff --git a/sdk-exporter/proto/src/main/protobuf/package.proto b/sdk-exporter/proto/src/main/protobuf/package.proto new file mode 100644 index 000000000..97fea9033 --- /dev/null +++ b/sdk-exporter/proto/src/main/protobuf/package.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +package opentelemetry.proto; + +import "scalapb/scalapb.proto"; + +// OpenTelemetry proto models are defined at https://github.com/open-telemetry/opentelemetry-proto. +// By default, generated models are public, which makes them accessible on the classpath. +// +// This poses a significant problem because opentelemetry-proto may introduce binary-breaking changes. +// As a result, any updates to the opentelemetry-proto can cause otel4s-sdk-exporter-proto to break binary compatibility. +// +// That's why we must generate them package-private. +// +// See: https://github.com/scalapb/ScalaPB/issues/1778, https://github.com/typelevel/otel4s/pull/860 +// +// Docs: +// https://scalapb.github.io/docs/faq/#how-do-i-mark-a-generated-case-class-private +// https://scalapb.github.io/docs/customizations/#auxiliary-options +option (scalapb.options) = { + scope: PACKAGE + + // override the package, so we can access the models + package_name: "org.typelevel.otel4s.sdk.exporter.proto" + + aux_message_options: [ + { + target: "*" + options: { + annotations: "private[exporter]" + companion_annotations: "private[exporter]" + } + } + ] + aux_enum_options: [ + { + target: "*" + options: { + base_annotations: "private[exporter]" + } + } + ] +}; diff --git a/sdk-exporter/proto/src/scalafix/resources/META-INF/services/scalafix.v1.Rule b/sdk-exporter/proto/src/scalafix/resources/META-INF/services/scalafix.v1.Rule new file mode 100644 index 000000000..e89ba5b9f --- /dev/null +++ b/sdk-exporter/proto/src/scalafix/resources/META-INF/services/scalafix.v1.Rule @@ -0,0 +1 @@ +fix.NoPublicClasses \ No newline at end of file diff --git a/sdk-exporter/proto/src/scalafix/scala/fix/NoPublicClasses.scala b/sdk-exporter/proto/src/scalafix/scala/fix/NoPublicClasses.scala new file mode 100644 index 000000000..e2c9000e6 --- /dev/null +++ b/sdk-exporter/proto/src/scalafix/scala/fix/NoPublicClasses.scala @@ -0,0 +1,30 @@ +package fix + +import scalafix.v1._ +import scala.meta._ + +class NoPublicClasses extends SyntacticRule("NoPublicClasses") { + override def description: String = "Disallows public classes and objects in the module" + + override def fix(implicit doc: SyntacticDocument): Patch = { + doc.tree.collect { + case defn: Defn.Class if !defn.mods.exists(_.is[Mod.Private]) && defn.parent.exists(_.is[Pkg.Body]) => + Patch.lint( + Diagnostic( + id = "NoPublicClasses", + message = "Public classes are not allowed in this module.", + position = defn.pos + ) + ) + + case defn: Defn.Object if !defn.mods.exists(_.is[Mod.Private]) && defn.parent.exists(_.is[Pkg.Body]) => + Patch.lint( + Diagnostic( + id = "NoPublicClasses", + message = "Public objects are not allowed in this module.", + position = defn.pos + ) + ) + }.asPatch + } +} diff --git a/sdk-exporter/trace/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansProtoEncoder.scala b/sdk-exporter/trace/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansProtoEncoder.scala index 4fa0d7d77..b3b83284d 100644 --- a/sdk-exporter/trace/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansProtoEncoder.scala +++ b/sdk-exporter/trace/src/main/scala/org/typelevel/otel4s/sdk/exporter/otlp/trace/SpansProtoEncoder.scala @@ -21,11 +21,11 @@ package trace import com.google.protobuf.ByteString import io.circe.Json -import io.opentelemetry.proto.collector.trace.v1.trace_service.ExportTraceServiceRequest -import io.opentelemetry.proto.trace.v1.trace.{Span => SpanProto} -import io.opentelemetry.proto.trace.v1.trace.{Status => StatusProto} -import io.opentelemetry.proto.trace.v1.trace.ResourceSpans -import io.opentelemetry.proto.trace.v1.trace.ScopeSpans +import org.typelevel.otel4s.sdk.exporter.proto.trace.{Span => SpanProto} +import org.typelevel.otel4s.sdk.exporter.proto.trace.{Status => StatusProto} +import org.typelevel.otel4s.sdk.exporter.proto.trace.ResourceSpans +import org.typelevel.otel4s.sdk.exporter.proto.trace.ScopeSpans +import org.typelevel.otel4s.sdk.exporter.proto.trace_service.ExportTraceServiceRequest import org.typelevel.otel4s.sdk.trace.data.EventData import org.typelevel.otel4s.sdk.trace.data.LinkData import org.typelevel.otel4s.sdk.trace.data.SpanData