diff --git a/README.md b/README.md index 1d9d8823e..4fdc39515 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Latest SBT dependency: `"org.broadinstitute.dsde.workbench" %% "workbench-util" Contains utility functions and classes. Util2 is added because util needs to support 2.11 for `firecloud-orchestration`, but many libraries start to drop 2.11 support. Util2 doesn't support 2.11. -Latest SBT dependency: `"org.broadinstitute.dsde.workbench" %% "workbench-util2" % "0.1-df50246"` +Latest SBT dependency: `"org.broadinstitute.dsde.workbench" %% "workbench-util2" % "0.1-TRAVIS-REPLACE-ME"` [Changelog](util2/CHANGELOG.md) @@ -82,7 +82,15 @@ Contains utility functions for publishing custom metrics using openTelemetry (op Latest SBT dependency: `"org.broadinstitute.dsde.workbench" %% "workbench-opentelemetry" % "0.1-e66171c"` -[Changelog](newrelic/CHANGELOG.md) +[Changelog](openTelemetry/CHANGELOG.md) + +## workbench-error-reporting + +Contains utility functions for publishing custom metrics using openTelemetry (openCensus and openTracing). + +Latest SBT dependency: `"org.broadinstitute.dsde.workbench" %% "workbench-error-reporting" % "0.1-TRAVIS-REPLACE-ME"` + +[Changelog](errorReporting/CHANGELOG.md) ## workbench-service-test diff --git a/build.sbt b/build.sbt index c423df225..09e1fe733 100644 --- a/build.sbt +++ b/build.sbt @@ -53,6 +53,12 @@ lazy val workbenchOpenTelemetry = project .dependsOn(workbenchUtil2 % testAndCompile) .withTestSettings +lazy val workbenchErrorReporting = project + .in(file("errorReporting")) + .settings(errorReportingSettings: _*) + .dependsOn(workbenchUtil2 % testAndCompile) + .withTestSettings + lazy val workbenchServiceTest = project .in(file("serviceTest")) .settings(serviceTestSettings: _*) @@ -82,6 +88,7 @@ lazy val workbenchLibs = project .aggregate(workbenchMetrics) .aggregate(workbenchNewrelic) .aggregate(workbenchOpenTelemetry) + .aggregate(workbenchErrorReporting) .aggregate(workbenchGoogle) .aggregate(workbenchGoogle2) .aggregate(workbenchServiceTest) diff --git a/errorReporting/CHANGELOG.md b/errorReporting/CHANGELOG.md new file mode 100644 index 000000000..cb3f24e51 --- /dev/null +++ b/errorReporting/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +This file documents changes to the `workbench-error-reporting` library, including notes on how to upgrade to new versions. + +## 0.1 + +### Added +- Add `ErrorReporting` + +SBT dependency: `"org.broadinstitute.dsde.workbench" %% "workbench-error-reporting" % "0.1-TRAVIS-REPLACE-ME"` diff --git a/errorReporting/src/main/scala/org/broadinstitute/dsde/workbench/errorReporting/ErrorReporting.scala b/errorReporting/src/main/scala/org/broadinstitute/dsde/workbench/errorReporting/ErrorReporting.scala new file mode 100644 index 000000000..c339f4c78 --- /dev/null +++ b/errorReporting/src/main/scala/org/broadinstitute/dsde/workbench/errorReporting/ErrorReporting.scala @@ -0,0 +1,50 @@ +package org.broadinstitute.dsde.workbench.errorReporting + +import java.nio.file.Path + +import cats.effect.{Resource, Sync} +import com.google.api.gax.core.FixedCredentialsProvider +import com.google.auth.oauth2.{GoogleCredentials, ServiceAccountCredentials} +import com.google.cloud.errorreporting.v1beta1.{ReportErrorsServiceClient, ReportErrorsServiceSettings} +import com.google.devtools.clouderrorreporting.v1beta1.{ProjectName, SourceLocation} + +import scala.collection.JavaConverters._ + +trait ErrorReporting[F[_]] { + def reportError(msg: String, sourceLocation: SourceLocation): F[Unit] + + /** + * @param t This throwable can not be NoStackTrace + * @return + */ + def reportError(t: Throwable): F[Unit] +} + +object ErrorReporting { + def fromPath[F[_]](pathToCredential: Path, appName: String, projectName: ProjectName)( + implicit F: Sync[F] + ): Resource[F, ErrorReporting[F]] = + for { + crendtialFile <- org.broadinstitute.dsde.workbench.util2.readPath(pathToCredential) + credential = ServiceAccountCredentials + .fromStream(crendtialFile) + .createScoped( + Set("https://www.googleapis.com/auth/cloud-platform").asJava + ) + client <- fromCredential(credential, appName, projectName) + } yield client + + def fromCredential[F[_]](credentials: GoogleCredentials, appName: String, projectName: ProjectName)( + implicit F: Sync[F] + ): Resource[F, ErrorReporting[F]] = { + val settings = ReportErrorsServiceSettings + .newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(credentials)) + .build() + Resource + .make[F, ReportErrorsServiceClient](F.delay(ReportErrorsServiceClient.create(settings)))( + c => F.delay(c.close()) + ) + .map(c => new ErrorReportingInterpreter[F](appName, projectName, c)) + } +} diff --git a/errorReporting/src/main/scala/org/broadinstitute/dsde/workbench/errorReporting/ErrorReportingInterpreter.scala b/errorReporting/src/main/scala/org/broadinstitute/dsde/workbench/errorReporting/ErrorReportingInterpreter.scala new file mode 100644 index 000000000..e0d2c41c7 --- /dev/null +++ b/errorReporting/src/main/scala/org/broadinstitute/dsde/workbench/errorReporting/ErrorReportingInterpreter.scala @@ -0,0 +1,46 @@ +package org.broadinstitute.dsde.workbench.errorReporting + +import cats.effect.{Resource, Sync} +import cats.implicits._ +import com.google.cloud.errorreporting.v1beta1.ReportErrorsServiceClient +import com.google.devtools.clouderrorreporting.v1beta1._ +import java.io.PrintWriter +import java.io.StringWriter + +class ErrorReportingInterpreter[F[_]](appName: String, projectName: ProjectName, client: ReportErrorsServiceClient)( + implicit F: Sync[F] +) extends ErrorReporting[F] { + val serviceContext = ServiceContext.newBuilder().setService(appName) + + override def reportError(msg: String, sourceLocation: SourceLocation): F[Unit] = { + val errorEvent = ReportedErrorEvent + .newBuilder() + .setMessage(msg) + .setServiceContext(serviceContext) + .setContext(ErrorContext.newBuilder().setReportLocation(sourceLocation)) + .build() + + F.delay(client.reportErrorEvent(projectName, errorEvent)) + } + + override def reportError(t: Throwable): F[Unit] = { + val stackTraceWriter = Resource.make { + val sw = new StringWriter + F.delay(StackTraceWriter(sw, new PrintWriter(sw))) + }(sw => F.delay(sw.printWriter.close()) >> F.delay(sw.stringWriter.close())) + + stackTraceWriter.use { w => + for { + _ <- F.delay(t.printStackTrace(w.printWriter)) + errorEvent = ReportedErrorEvent + .newBuilder() + .setMessage(w.stringWriter.toString) + .setServiceContext(serviceContext) + .build() + _ <- F.delay(client.reportErrorEvent(projectName, errorEvent)) + } yield () + } + } +} + +final private case class StackTraceWriter(stringWriter: StringWriter, printWriter: PrintWriter) diff --git a/errorReporting/src/main/scala/org/broadinstitute/dsde/workbench/errorReporting/ReportWorthy.scala b/errorReporting/src/main/scala/org/broadinstitute/dsde/workbench/errorReporting/ReportWorthy.scala new file mode 100644 index 000000000..ed845d667 --- /dev/null +++ b/errorReporting/src/main/scala/org/broadinstitute/dsde/workbench/errorReporting/ReportWorthy.scala @@ -0,0 +1,16 @@ +package org.broadinstitute.dsde.workbench.errorReporting + +/** + * Defines a ReportWorthy type class which has `def isReportWorthy(a: A): Boolean` function + */ +trait ReportWorthy[A] { + def isReportWorthy(a: A): Boolean +} + +final case class ReportWorthyOps[A](a: A)(implicit ev: ReportWorthy[A]) { + def isReportWorthy: Boolean = ev.isReportWorthy(a) +} + +object ReportWorthySyntax { + implicit def reportWorthySyntax[A: ReportWorthy](a: A): ReportWorthyOps[A] = ReportWorthyOps(a) +} diff --git a/errorReporting/src/test/scala/org/broadinstitute/dsde/workbench/errorReporting/ErrorReportingManualTest.scala b/errorReporting/src/test/scala/org/broadinstitute/dsde/workbench/errorReporting/ErrorReportingManualTest.scala new file mode 100644 index 000000000..62e727a20 --- /dev/null +++ b/errorReporting/src/test/scala/org/broadinstitute/dsde/workbench/errorReporting/ErrorReportingManualTest.scala @@ -0,0 +1,39 @@ +package org.broadinstitute.dsde.workbench.errorReporting + +import java.nio.file.Paths + +import cats.effect.IO +import com.google.devtools.clouderrorreporting.v1beta1.{ProjectName, SourceLocation} + +import scala.concurrent.ExecutionContext.global +import scala.util.control.NoStackTrace + +object ErrorReportingManualTest { + implicit val cs = IO.contextShift(global) + + private def test(reporting: ErrorReporting[IO]): IO[Unit] = + for { + _ <- reporting.reportError(new Exception("eeee2")) + _ <- reporting.reportError( + "error2", + SourceLocation + .newBuilder() + .setFunctionName("qi-function") + .setFilePath(this.getClass.getName) +// .setLineNumber(10) + .build() + ) + } yield () + + def run(): Unit = { + val res = ErrorReporting + .fromPath[IO](Paths.get("/Users/qi/.google/qi-billing-90828dd5e7b8.json"), + "qi-test-app", + ProjectName.of("qi-billing")) + .use(c => test(c)) + + res.unsafeRunSync() + } +} + +final case class CustomException(msg: String) extends NoStackTrace diff --git a/errorReporting/src/test/scala/org/broadinstitute/dsde/workbench/errorReporting/FakeErrorReporting.scala b/errorReporting/src/test/scala/org/broadinstitute/dsde/workbench/errorReporting/FakeErrorReporting.scala new file mode 100644 index 000000000..f1afde437 --- /dev/null +++ b/errorReporting/src/test/scala/org/broadinstitute/dsde/workbench/errorReporting/FakeErrorReporting.scala @@ -0,0 +1,10 @@ +package org.broadinstitute.dsde.workbench.errorReporting + +import cats.effect._ +import com.google.devtools.clouderrorreporting.v1beta1.SourceLocation + +object FakeErrorReporting extends ErrorReporting[IO] { + override def reportError(msg: String, sourceLocation: SourceLocation): IO[Unit] = IO.unit + + override def reportError(t: Throwable): IO[Unit] = IO.unit +} diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 57c49cd9f..9e89d74a8 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -189,6 +189,11 @@ object Dependencies { openCensusTraceStackDriver ) + val errorReportingDependencies = List( + catsEffect, + "com.google.cloud" % "google-cloud-errorreporting" % "0.120.0-beta" + ) + val util2Dependencies = commonDependencies ++ List( catsEffect, log4cats, diff --git a/project/Settings.scala b/project/Settings.scala index f1a8fc8b6..381f2aacc 100644 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -207,6 +207,12 @@ object Settings { version := createVersion("0.1") ) ++ publishSettings + val errorReportingSettings = cross212and213 ++ commonSettings ++ List( + name := "workbench-error-reporting", + libraryDependencies ++= errorReportingDependencies, + version := createVersion("0.1") + ) ++ publishSettings + val serviceTestSettings = only212 ++ commonSettings ++ List( name := "workbench-service-test", libraryDependencies ++= serviceTestDependencies, diff --git a/util2/CHANGELOG.md b/util2/CHANGELOG.md index 7c66daca2..7ae75aff4 100644 --- a/util2/CHANGELOG.md +++ b/util2/CHANGELOG.md @@ -6,6 +6,6 @@ This file documents changes to the `workbench-util2` library, including notes on - Add `ConsoleLogger` -SBT dependency: `"org.broadinstitute.dsde.workbench" %% "workbench-util2" % "0.1-df50246"` +SBT dependency: `"org.broadinstitute.dsde.workbench" %% "workbench-util2" % "0.1-TRAVIS-REPLACE-ME"` Moved a few utilities that depends on `circe`, `fs2` from `util` to `util2` diff --git a/util2/src/main/scala/org/broadinstitute/dsde/workbench/util2/package.scala b/util2/src/main/scala/org/broadinstitute/dsde/workbench/util2/package.scala index c02aafeab..877187cff 100644 --- a/util2/src/main/scala/org/broadinstitute/dsde/workbench/util2/package.scala +++ b/util2/src/main/scala/org/broadinstitute/dsde/workbench/util2/package.scala @@ -26,6 +26,9 @@ package object util2 { def readFile[F[_]](path: String)(implicit sf: Sync[F]): Resource[F, FileInputStream] = Resource.make(sf.delay(new FileInputStream(path)))(f => sf.delay(f.close())) + def readPath[F[_]](path: Path)(implicit sf: Sync[F]): Resource[F, FileInputStream] = + Resource.make(sf.delay(new FileInputStream(path.toString)))(f => sf.delay(f.close())) + /* * Example: * scala> org.broadinstitute.dsde.workbench.util.readJsonFileToA[IO, List[String]](java.nio.file.Paths.get("/tmp/list"), None).compile.lastOrError.unsafeRunSync