├── .circleci └── config.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── NOTICE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main └── scala │ └── au │ └── com │ └── titanclass │ └── streams │ └── telemetry │ ├── MetricProtobufMarshalling.scala │ ├── SpanProtobufMarshalling.scala │ ├── StreamMetricExporter.scala │ └── StreamSpanExporter.scala └── test └── scala └── au └── com └── titanclass └── streams └── telemetry ├── MetricProtobufMarshallingTest.scala ├── StreamMetricExporterTest.scala ├── StreamSpanExporterTest.scala └── TracingProtobufMarshallingTest.scala /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/project 5 | docker: 6 | - image: circleci/openjdk:8 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | key: sbt-cache-1 11 | - run: 12 | name: Compile & Test 13 | command: | 14 | sbt clean compile test:compile test 15 | - save_cache: 16 | key: sbt-cache-1 17 | paths: 18 | - "~/.ivy2/cache" 19 | - "~/.sbt" 20 | - "~/.m2" 21 | workflows: 22 | version: 2 23 | build: 24 | jobs: 25 | - build 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # sbt 2 | lib_managed 3 | project/project 4 | target 5 | 6 | # Worksheets (Eclipse or IntelliJ) 7 | *.sc 8 | 9 | # Eclipse 10 | .cache* 11 | .classpath 12 | .project 13 | .scala_dependencies 14 | .settings 15 | .target 16 | .worksheet 17 | 18 | # IntelliJ 19 | .idea 20 | 21 | # ENSIME 22 | .ensime 23 | .ensime_lucene 24 | .ensime_cache 25 | 26 | # Mac 27 | .DS_Store 28 | 29 | # Akka 30 | ddata* 31 | journal 32 | snapshots 33 | 34 | # Log files 35 | *.log 36 | 37 | # jenv 38 | .java-version 39 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 2.6.3 2 | 3 | preset = "defaultWithAlign" 4 | 5 | danglingParentheses.preset = true 6 | indentOperator.preset = spray 7 | maxColumn = 100 8 | newlines.alwaysBeforeMultilineDef = true 9 | project.excludeFilters = [".*\\.sbt"] 10 | rewrite.rules = [AsciiSortImports, RedundantBraces, RedundantParens] 11 | spaces.inImportCurlyBraces = true 12 | unindentTopLevelOperators = true 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2019 huntc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reactive-streams-telemetry # 2 | 3 | [![maven-central-badge][]][maven-central] [![build-badge][]][build] 4 | 5 | [maven-central]: https://search.maven.org/#search%7Cga%7C1%7Creactive-streams-telemetry 6 | [maven-central-badge]: https://maven-badges.herokuapp.com/maven-central/au.com.titanclass/reactive-streams-telemetry_2.13/badge.svg 7 | [build]: https://circleci.com/gh/titanclass/reactive-streams-telemetry 8 | [build-badge]: https://circleci.com/gh/titanclass/reactive-streams-telemetry.svg?style=shield 9 | 10 | Welcome to reactive-streams-telemetry! 11 | 12 | The goal of this [Scala](https://www.scala-lang.org/)/Java library is to provide a [Reactive Streams](http://www.reactive-streams.org/) interface to [Open Telemetry](https://github.com/open-telemetry/opentelemetry-java) metrics and traces so that low memory utilization can be attained. 13 | 14 | Metrics and traces are presented using streams where elements pertain to Open Telemetry's [MetricData](https://github.com/open-telemetry/opentelemetry-java/blob/master/sdk/src/main/java/io/opentelemetry/sdk/metrics/data/MetricData.java) 15 | and [SpanData](https://github.com/open-telemetry/opentelemetry-java/blob/master/sdk/src/main/java/io/opentelemetry/sdk/trace/data/SpanData.java) classes respectively. 16 | Applications can consume these streams and present them 17 | however they wish e.g. [Akka HTTP](https://doc.akka.io/docs/akka-http/current/) can be used to serve a snapshot of metrics and traces as JSON 18 | with an HTTP `GET` route. Another example could be where metrics and traces are published 19 | over UDP to your favorite collection engine. 20 | 21 | [Akka Streams](https://doc.akka.io/docs/akka/2.6/stream/) 22 | is used as the Reactive Streams interface and implementation. 23 | 24 | Other than the libraries declared above, there are no additional dependencies. 25 | 26 | ## Teaser 27 | 28 | Serve up the latest telemetry gathered given an [Alpakka Unix Domain Socket](https://doc.akka.io/docs/alpakka/current/unix-domain-socket.html) 29 | and the establishment of the `metrics` and `traces` sources from their respective exporters: 30 | 31 | ```scala 32 | import akka.NotUsed 33 | import akka.stream.alpakka.unixdomainsocket.scaladsl.UnixDomainSocket 34 | import akka.stream.scaladsl.{ Flow, Sink, Source } 35 | import au.com.titanclass.streams.telemetry.{ MetricProtobufMarshalling, SpanProtobufMarshalling } 36 | import io.opentelemetry.sdk.metrics.data.MetricData 37 | import io.opentelemetry.sdk.trace.data.SpanData 38 | import java.io.File 39 | 40 | val metrics: Source[MetricData, NotUsed] = ??? 41 | val traces: Source[SpanData, NotUsed] = ??? 42 | 43 | val source = 44 | metrics 45 | .map { metricData => 46 | import MetricProtobufMarshalling._ 47 | metricData.toProtobuf.build().toString 48 | } 49 | .merge( 50 | traces 51 | .map { spanData => 52 | import SpanProtobufMarshalling._ 53 | spanData.toProtobuf.build().toString 54 | } 55 | ) 56 | 57 | UnixDomainSocket() 58 | .bindAndHandle(Flow.fromSinkAndSourceCoupled(Sink.ignore, source), 59 | new File("/var/run/mysocket.sock")) 60 | ``` 61 | 62 | The above will just output the string representations of each element of telemetry. 63 | 64 | ## Download 65 | 66 | Builds are published to Maven Central. Please substitute `version` accordingly. 67 | 68 | ``` 69 | "au.com.titanclass" %% "reactive-streams-telemetry" % version 70 | ``` 71 | 72 | ## Usage 73 | 74 | Please check out the tests for sample usage. 75 | 76 | ## Contribution policy ## 77 | 78 | Contributions via GitHub pull requests are gladly accepted from their original author. Along with 79 | any pull requests, please state that the contribution is your original work and that you license 80 | the work to the project under the project's open source license. Whether or not you state this 81 | explicitly, by submitting any copyrighted material via pull request, email, or other means you 82 | agree to license the material under the project's open source license and warrant that you have the 83 | legal authority to do so. 84 | 85 | ## Publishing ## 86 | 87 | You'll need a GPG key to sign as artifacts are published to Sonatype for publishing at Maven Central. 88 | Once you have GPG setup: 89 | 90 | ``` 91 | export GPG_TTY=$(tty) 92 | sbt publishSigned 93 | ``` 94 | 95 | ## License ## 96 | 97 | This code is open source software licensed under the 98 | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) license. 99 | 100 | (c) Copyright Titan Class Pty Ltd, 2019 -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | // ***************************************************************************** 2 | // Projects 3 | // ***************************************************************************** 4 | 5 | lazy val `reactive-streams-telemetry` = 6 | project 7 | .in(file(".")) 8 | .enablePlugins(AutomateHeaderPlugin) 9 | .settings(settings) 10 | .settings( 11 | libraryDependencies ++= Seq( 12 | library.akkaStream, 13 | library.openTelemetryProto, 14 | library.openTelemetrySdk, 15 | library.utest % Test, 16 | library.akkaStreamTestkit % Test 17 | ) 18 | ) 19 | 20 | // ***************************************************************************** 21 | // Library dependencies 22 | // ***************************************************************************** 23 | 24 | lazy val library = 25 | new { 26 | object Version { 27 | val akka = "2.6.9" 28 | val openTelemetryProto = "0.3.0" 29 | val openTelemetrySdk = "0.8.0" 30 | val utest = "0.7.5" 31 | } 32 | val akkaStream = "com.typesafe.akka" %% "akka-stream" % Version.akka 33 | val akkaStreamTestkit = "com.typesafe.akka" %% "akka-stream-testkit" % Version.akka 34 | val openTelemetryProto = "io.opentelemetry" % "opentelemetry-proto" % Version.openTelemetryProto 35 | val openTelemetrySdk = "io.opentelemetry" % "opentelemetry-sdk" % Version.openTelemetrySdk 36 | val utest = "com.lihaoyi" %% "utest" % Version.utest 37 | } 38 | 39 | // ***************************************************************************** 40 | // Settings 41 | // ***************************************************************************** 42 | 43 | lazy val settings = 44 | commonSettings ++ scalafmtSettings 45 | 46 | lazy val commonSettings = 47 | Seq( 48 | scalaVersion := "2.13.3", 49 | organization := "au.com.titanclass", 50 | organizationName := "Titan Class Pty Ltd", 51 | organizationHomepage := Some(url("https://www.titanclass.com.au")), 52 | startYear := Some(2019), 53 | licenses += ("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0")), 54 | homepage := Some(url("https://github.com/titanclass/reactive-streams-telemetry")), 55 | scmInfo := Some( 56 | ScmInfo( 57 | url("https://github.com/titanclass/reactive-streams-telemetry"), 58 | "scm:git@github.com:titanclass/reactive-streams-telemetry.git" 59 | ) 60 | ), 61 | developers := List( 62 | Developer( 63 | id = "huntc", 64 | name = "Christopher Hunt", 65 | email = "huntchr@gmail.com", 66 | url = url("http://christopherhunt-software.blogspot.com/") 67 | ) 68 | ), 69 | scalacOptions ++= Seq( 70 | "-unchecked", 71 | "-deprecation", 72 | "-language:_", 73 | "-target:jvm-1.8", 74 | "-encoding", "UTF-8" 75 | ), 76 | Compile / unmanagedSourceDirectories := Seq((Compile / scalaSource).value), 77 | Test / unmanagedSourceDirectories := Seq((Test / scalaSource).value), 78 | testFrameworks += new TestFramework("utest.runner.Framework"), 79 | wartremoverWarnings in (Compile, compile) ++= Warts.unsafe, 80 | 81 | // Maven Central publishing 82 | credentials += Credentials(Path.userHome / ".sbt" / "sonatype_credential"), 83 | pomIncludeRepository := { _ => false }, 84 | publishTo := { 85 | val nexus = "https://oss.sonatype.org/" 86 | if (isSnapshot.value) Some("snapshots" at nexus + "content/repositories/snapshots") 87 | else Some("releases" at nexus + "service/local/staging/deploy/maven2") 88 | }, 89 | publishMavenStyle := true 90 | ) 91 | 92 | lazy val scalafmtSettings = 93 | Seq( 94 | scalafmtOnCompile := true, 95 | ) 96 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.3.13 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1") 2 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.0") 3 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.5.0") 4 | addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.4.10") 5 | addSbtPlugin("com.typesafe.sbt" % "sbt-license-report" % "1.2.0") 6 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.1") 7 | -------------------------------------------------------------------------------- /src/main/scala/au/com/titanclass/streams/telemetry/MetricProtobufMarshalling.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Titan Class Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package au.com.titanclass.streams.telemetry 18 | 19 | import io.opentelemetry.common.ReadableKeyValuePairs 20 | import io.opentelemetry.proto.common.v1.StringKeyValue 21 | import io.opentelemetry.proto.metrics.v1.{ 22 | DoubleDataPoint, 23 | Int64DataPoint, 24 | Metric, 25 | MetricDescriptor, 26 | SummaryDataPoint 27 | } 28 | import io.opentelemetry.sdk.metrics.data.MetricData 29 | 30 | import scala.collection.mutable.ArrayBuffer 31 | import scala.jdk.CollectionConverters._ 32 | 33 | /** 34 | * Marshalls metric data to the Open Telemetry protobuf form 35 | */ 36 | object MetricProtobufMarshalling { 37 | implicit class Marshaller(metricData: MetricData) { 38 | def toProtobuf: Metric.Builder = { 39 | val metricDataDescriptor = metricData.getDescriptor 40 | val doubleDataPoints = new ArrayBuffer[DoubleDataPoint](metricData.getPoints.size()) 41 | val int64DataPoints = new ArrayBuffer[Int64DataPoint](metricData.getPoints.size()) 42 | val summaryDataPoints = new ArrayBuffer[SummaryDataPoint](metricData.getPoints.size()) 43 | metricData.getPoints.forEach { 44 | case p: MetricData.DoublePoint => 45 | val _ = doubleDataPoints += DoubleDataPoint 46 | .newBuilder() 47 | .addAllLabels(stringKeyValues(p.getLabels).asJava) 48 | .setStartTimeUnixNano(p.getStartEpochNanos) 49 | .setTimeUnixNano(p.getEpochNanos) 50 | .setValue(p.getValue) 51 | .build() 52 | case p: MetricData.LongPoint => 53 | val _ = int64DataPoints += Int64DataPoint 54 | .newBuilder() 55 | .addAllLabels(stringKeyValues(p.getLabels).asJava) 56 | .setStartTimeUnixNano(p.getStartEpochNanos) 57 | .setTimeUnixNano(p.getEpochNanos) 58 | .setValue(p.getValue) 59 | .build() 60 | case p: MetricData.SummaryPoint => 61 | val _ = summaryDataPoints += SummaryDataPoint 62 | .newBuilder() 63 | .addAllLabels(stringKeyValues(p.getLabels).asJava) 64 | .setStartTimeUnixNano(p.getStartEpochNanos) 65 | .setTimeUnixNano(p.getEpochNanos) 66 | .setCount(p.getCount) 67 | .addAllPercentileValues(p.getPercentileValues.asScala.map { pv => 68 | SummaryDataPoint.ValueAtPercentile 69 | .newBuilder() 70 | .setPercentile(pv.getPercentile) 71 | .setValue(pv.getValue) 72 | .build() 73 | }.asJava) 74 | .build() 75 | } 76 | Metric 77 | .newBuilder() 78 | .setMetricDescriptor( 79 | MetricDescriptor 80 | .newBuilder() 81 | .setDescription(metricDataDescriptor.getDescription) 82 | .addAllLabels(stringKeyValues(metricDataDescriptor.getConstantLabels).asJava) 83 | .setName(metricDataDescriptor.getName) 84 | .setType(metricDataDescriptor.getType match { 85 | case MetricData.Descriptor.Type.MONOTONIC_DOUBLE => 86 | MetricDescriptor.Type.COUNTER_DOUBLE 87 | case MetricData.Descriptor.Type.MONOTONIC_LONG => 88 | MetricDescriptor.Type.COUNTER_INT64 89 | case MetricData.Descriptor.Type.NON_MONOTONIC_DOUBLE => 90 | MetricDescriptor.Type.GAUGE_DOUBLE 91 | case MetricData.Descriptor.Type.NON_MONOTONIC_LONG => 92 | MetricDescriptor.Type.GAUGE_INT64 93 | case MetricData.Descriptor.Type.SUMMARY => 94 | MetricDescriptor.Type.SUMMARY 95 | }) 96 | .setUnit(metricDataDescriptor.getUnit) 97 | ) 98 | .addAllDoubleDataPoints(doubleDataPoints.asJava) 99 | .addAllInt64DataPoints(int64DataPoints.asJava) 100 | .addAllSummaryDataPoints(summaryDataPoints.asJava) 101 | } 102 | } 103 | 104 | private def stringKeyValues(kvs: ReadableKeyValuePairs[String]): ArrayBuffer[StringKeyValue] = { 105 | val array = new ArrayBuffer[StringKeyValue](kvs.size()) 106 | kvs.forEach { (k, v) => 107 | val _ = array += StringKeyValue.newBuilder().setKey(k).setValue(v).build() 108 | } 109 | array 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/scala/au/com/titanclass/streams/telemetry/SpanProtobufMarshalling.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Titan Class Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package au.com.titanclass.streams.telemetry 18 | 19 | import com.google.protobuf.ByteString 20 | import io.opentelemetry.common.{ AttributeValue, ReadableKeyValuePairs } 21 | import io.opentelemetry.proto.common.v1.AttributeKeyValue 22 | import io.opentelemetry.proto.trace.v1.{ Span => ProtoSpan, Status => ProtoStatus } 23 | import io.opentelemetry.sdk.trace.data.SpanData 24 | import io.opentelemetry.trace.{ Span, SpanId, Status } 25 | 26 | import scala.collection.mutable.ArrayBuffer 27 | import scala.jdk.CollectionConverters._ 28 | 29 | /** 30 | * Marshalls span data to the Open Telemetry protobuf form 31 | */ 32 | object SpanProtobufMarshalling { 33 | implicit class Marshaller(spanData: SpanData) { 34 | def toProtobuf: ProtoSpan.Builder = 35 | ProtoSpan 36 | .newBuilder() 37 | .addAllAttributes(attributeKeyValues(spanData.getAttributes).asJava) 38 | .setDroppedAttributesCount(spanData.getTotalAttributeCount - spanData.getAttributes.size()) 39 | .setDroppedEventsCount(spanData.getTotalRecordedEvents - spanData.getEvents.size()) 40 | .setDroppedLinksCount(spanData.getTotalRecordedLinks - spanData.getLinks.size()) 41 | .setEndTimeUnixNano(spanData.getEndEpochNanos) 42 | .addAllEvents { 43 | spanData.getEvents.asScala.map { e => 44 | ProtoSpan.Event 45 | .newBuilder() 46 | .addAllAttributes(attributeKeyValues(e.getAttributes).asJava) 47 | .setDroppedAttributesCount(e.getTotalAttributeCount - e.getAttributes.size()) 48 | .setName(e.getName) 49 | .setTimeUnixNano(e.getEpochNanos) 50 | .build() 51 | }.asJava 52 | } 53 | .setKind { 54 | spanData.getKind match { 55 | case Span.Kind.CLIENT => ProtoSpan.SpanKind.CLIENT 56 | case Span.Kind.CONSUMER => ProtoSpan.SpanKind.CONSUMER 57 | case Span.Kind.INTERNAL => ProtoSpan.SpanKind.INTERNAL 58 | case Span.Kind.PRODUCER => ProtoSpan.SpanKind.PRODUCER 59 | case Span.Kind.SERVER => ProtoSpan.SpanKind.SERVER 60 | } 61 | } 62 | .addAllLinks { 63 | spanData.getLinks.asScala.map { e => 64 | ProtoSpan.Link 65 | .newBuilder() 66 | .addAllAttributes(attributeKeyValues(e.getAttributes).asJava) 67 | .setDroppedAttributesCount(e.getTotalAttributeCount - e.getAttributes.size()) 68 | .build() 69 | }.asJava 70 | } 71 | .setName(spanData.getName) 72 | .setParentSpanId(spanId(spanData.getParentSpanId)) 73 | .setSpanId(spanId(spanData.getSpanId)) 74 | .setStartTimeUnixNano(spanData.getStartEpochNanos) 75 | .setStatus { 76 | val status = spanData.getStatus match { 77 | case Status.ABORTED => 78 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.Aborted) 79 | case Status.ALREADY_EXISTS => 80 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.AlreadyExists) 81 | case Status.CANCELLED => 82 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.Cancelled) 83 | case Status.DATA_LOSS => 84 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.DataLoss) 85 | case Status.DEADLINE_EXCEEDED => 86 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.DeadlineExceeded) 87 | case Status.FAILED_PRECONDITION => 88 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.FailedPrecondition) 89 | case Status.INTERNAL => 90 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.InternalError) 91 | case Status.INVALID_ARGUMENT => 92 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.InvalidArgument) 93 | case Status.NOT_FOUND => 94 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.NotFound) 95 | case Status.OK => 96 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.Ok) 97 | case Status.OUT_OF_RANGE => 98 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.OutOfRange) 99 | case Status.PERMISSION_DENIED => 100 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.PermissionDenied) 101 | case Status.RESOURCE_EXHAUSTED => 102 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.ResourceExhausted) 103 | case Status.UNAUTHENTICATED => 104 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.Unauthenticated) 105 | case Status.UNAVAILABLE => 106 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.Unavailable) 107 | case Status.UNIMPLEMENTED => 108 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.Unimplemented) 109 | case Status.UNKNOWN => 110 | ProtoStatus.newBuilder().setCode(ProtoStatus.StatusCode.UnknownError) 111 | } 112 | val desc = spanData.getStatus.getDescription 113 | if (desc != null) status.setMessage(spanData.getStatus.getDescription) else status 114 | } 115 | .setTraceId { 116 | val bytes = Array.ofDim[Byte](16) 117 | spanData.getTraceId.copyBytesTo(bytes, 0) 118 | ByteString.copyFrom(bytes) 119 | } 120 | .setTraceState { 121 | spanData.getTraceState.getEntries.asScala 122 | .map(e => e.getKey + "=" + e.getValue) 123 | .mkString(",") 124 | } 125 | } 126 | 127 | private def attributeKeyValues( 128 | kvs: ReadableKeyValuePairs[AttributeValue] 129 | ): ArrayBuffer[AttributeKeyValue] = { 130 | val array = new ArrayBuffer[AttributeKeyValue](kvs.size()) 131 | kvs.forEach { (k, v) => 132 | val b = AttributeKeyValue.newBuilder().setKey(k) 133 | 134 | { 135 | val _ = v.getType match { 136 | case AttributeValue.Type.STRING => b.setStringValue(v.getStringValue) 137 | case AttributeValue.Type.BOOLEAN => b.setBoolValue(v.getBooleanValue) 138 | case AttributeValue.Type.LONG => b.setIntValue(v.getLongValue) 139 | case AttributeValue.Type.DOUBLE => b.setDoubleValue(v.getDoubleValue) 140 | case AttributeValue.Type.STRING_ARRAY => // FIXME: When released https://github.com/open-telemetry/opentelemetry-proto/commit/e43e1abc40428a6ee98e3bfd79bec1dfa2ed18cd 141 | case AttributeValue.Type.BOOLEAN_ARRAY => // FIXME: When released https://github.com/open-telemetry/opentelemetry-proto/commit/e43e1abc40428a6ee98e3bfd79bec1dfa2ed18cd 142 | case AttributeValue.Type.LONG_ARRAY => // FIXME: When released https://github.com/open-telemetry/opentelemetry-proto/commit/e43e1abc40428a6ee98e3bfd79bec1dfa2ed18cd 143 | case AttributeValue.Type.DOUBLE_ARRAY => // FIXME: When released https://github.com/open-telemetry/opentelemetry-proto/commit/e43e1abc40428a6ee98e3bfd79bec1dfa2ed18cd 144 | } 145 | } 146 | 147 | { 148 | val _ = array += b.build() 149 | } 150 | } 151 | array 152 | } 153 | 154 | private def spanId(spanId: SpanId): ByteString = { 155 | val bytes = Array.ofDim[Byte](8) 156 | spanId.copyBytesTo(bytes, 0) 157 | ByteString.copyFrom(bytes) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/scala/au/com/titanclass/streams/telemetry/StreamMetricExporter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Titan Class Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package au.com.titanclass.streams.telemetry 18 | import java.util 19 | 20 | import akka.NotUsed 21 | import akka.stream.{ Attributes, Materializer, OverflowStrategy } 22 | import akka.stream.scaladsl.{ BroadcastHub, Keep, Source } 23 | import io.opentelemetry.sdk.common.CompletableResultCode 24 | import io.opentelemetry.sdk.metrics.`export`.MetricExporter 25 | import io.opentelemetry.sdk.metrics.data.MetricData 26 | 27 | import scala.concurrent.Future 28 | import scala.jdk.CollectionConverters._ 29 | import scala.util.{ Failure, Success } 30 | 31 | object StreamMetricExporter { 32 | def apply(bufferSize: Int)(implicit mat: Materializer): StreamMetricExporter = 33 | new StreamMetricExporter(bufferSize) 34 | } 35 | 36 | /** 37 | * Provides a source of metrics 38 | */ 39 | class StreamMetricExporter(bufferSize: Int)(implicit mat: Materializer) extends MetricExporter { 40 | 41 | private val (metricQueue, metricSource) = Source 42 | .queue[MetricData](bufferSize, OverflowStrategy.dropHead.withLogLevel(Attributes.LogLevels.Off)) 43 | .toMat(BroadcastHub.sink(1))(Keep.both) 44 | .run() 45 | 46 | /** 47 | * Returns a source of metrics 48 | */ 49 | def source: Source[MetricData, NotUsed] = 50 | metricSource 51 | 52 | override def `export`(metrics: util.Collection[MetricData]): CompletableResultCode = { 53 | val resultCode = new CompletableResultCode() 54 | import mat.executionContext 55 | Future.sequence(metrics.asScala.map(x => metricQueue.offer(x)).toList).onComplete { 56 | case Success(_) => resultCode.succeed() 57 | case Failure(_) => resultCode.fail() 58 | } 59 | resultCode 60 | } 61 | 62 | override def flush(): CompletableResultCode = 63 | CompletableResultCode.ofSuccess() 64 | 65 | override def shutdown(): Unit = 66 | metricQueue.complete() 67 | } 68 | -------------------------------------------------------------------------------- /src/main/scala/au/com/titanclass/streams/telemetry/StreamSpanExporter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Titan Class Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package au.com.titanclass.streams.telemetry 18 | import java.util 19 | 20 | import akka.NotUsed 21 | import akka.stream.{ Attributes, Materializer, OverflowStrategy } 22 | import akka.stream.scaladsl.{ BroadcastHub, Keep, Source } 23 | import io.opentelemetry.sdk.common.CompletableResultCode 24 | import io.opentelemetry.sdk.trace.`export`.SpanExporter 25 | import io.opentelemetry.sdk.trace.data.SpanData 26 | 27 | import scala.concurrent.Future 28 | import scala.jdk.CollectionConverters._ 29 | import scala.util.{ Failure, Success } 30 | 31 | object StreamSpanExporter { 32 | def apply(bufferSize: Int)(implicit mat: Materializer): StreamSpanExporter = 33 | new StreamSpanExporter(bufferSize) 34 | } 35 | 36 | /** 37 | * Provides a source of trace spans 38 | */ 39 | class StreamSpanExporter(bufferSize: Int)(implicit mat: Materializer) extends SpanExporter { 40 | 41 | private val (tracerQueue, tracerSource) = Source 42 | .queue[SpanData](bufferSize, OverflowStrategy.dropHead.withLogLevel(Attributes.LogLevels.Off)) 43 | .toMat(BroadcastHub.sink(1))(Keep.both) 44 | .run() 45 | 46 | /** 47 | * Returns a source of spans 48 | */ 49 | def source: Source[SpanData, NotUsed] = 50 | tracerSource 51 | 52 | override def `export`(spans: util.Collection[SpanData]): CompletableResultCode = { 53 | val resultCode = new CompletableResultCode() 54 | import mat.executionContext 55 | Future.sequence(spans.asScala.map(x => tracerQueue.offer(x)).toList).onComplete { 56 | case Success(_) => resultCode.succeed() 57 | case Failure(_) => resultCode.fail() 58 | } 59 | resultCode 60 | } 61 | 62 | override def flush(): CompletableResultCode = 63 | CompletableResultCode.ofSuccess() 64 | 65 | override def shutdown(): CompletableResultCode = { 66 | val resultCode = new CompletableResultCode() 67 | tracerQueue.complete() 68 | import mat.executionContext 69 | tracerQueue.watchCompletion().foreach(_ => resultCode.succeed()) 70 | resultCode 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/scala/au/com/titanclass/streams/telemetry/MetricProtobufMarshallingTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Titan Class Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package au.com.titanclass.streams.telemetry 18 | 19 | import io.opentelemetry.common.Labels 20 | import io.opentelemetry.sdk.common.InstrumentationLibraryInfo 21 | import io.opentelemetry.sdk.metrics.data.MetricData 22 | import io.opentelemetry.sdk.metrics.data.MetricData.Descriptor 23 | import io.opentelemetry.sdk.resources.Resource 24 | import utest._ 25 | 26 | object MetricProtobufMarshallingTest extends TestSuite { 27 | 28 | val tests: Tests = Tests { 29 | test("write metrics") { 30 | val metricData = MetricData.create( 31 | Descriptor.create( 32 | "name", 33 | "description", 34 | "1", 35 | Descriptor.Type.MONOTONIC_LONG, 36 | Labels.empty() 37 | ), 38 | Resource.getEmpty, 39 | InstrumentationLibraryInfo.getEmpty, 40 | java.util.Collections.emptyList() 41 | ) 42 | 43 | import MetricProtobufMarshalling._ 44 | val metric = metricData.toProtobuf.build() 45 | metric.getMetricDescriptor.getName ==> "name" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/scala/au/com/titanclass/streams/telemetry/StreamMetricExporterTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Titan Class Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package au.com.titanclass.streams.telemetry 18 | 19 | import akka.actor.ActorSystem 20 | import akka.stream.testkit.scaladsl.TestSink 21 | import io.opentelemetry.common.Labels 22 | import io.opentelemetry.sdk.common.InstrumentationLibraryInfo 23 | import io.opentelemetry.sdk.metrics.data.MetricData 24 | import io.opentelemetry.sdk.metrics.data.MetricData.{ Descriptor, LongPoint } 25 | import io.opentelemetry.sdk.resources.Resource 26 | import utest._ 27 | 28 | import scala.concurrent.ExecutionContext 29 | import scala.jdk.CollectionConverters._ 30 | 31 | object StreamMetricExporterTest extends TestSuite { 32 | 33 | implicit lazy val system: ActorSystem = 34 | ActorSystem("metrics-reporter-tests") 35 | 36 | override def utestAfterAll(): Unit = 37 | system.terminate() 38 | 39 | implicit lazy val ec: ExecutionContext = 40 | system.dispatcher 41 | 42 | val tests: Tests = Tests { 43 | test("consume and shutdown") { 44 | val metric = MetricData.create( 45 | Descriptor.create( 46 | "name", 47 | "description", 48 | "1", 49 | Descriptor.Type.MONOTONIC_LONG, 50 | Labels.empty() 51 | ), 52 | Resource.getEmpty, 53 | InstrumentationLibraryInfo.getEmpty, 54 | java.util.Collections.emptyList() 55 | ) 56 | val metrics = List(metric).asJava 57 | 58 | val exporter = StreamMetricExporter(1) 59 | 60 | val sourceSubscriber = exporter.source.runWith(TestSink.probe[MetricData]) 61 | 62 | exporter.`export`(metrics) 63 | 64 | sourceSubscriber.request(1).expectNext(metric) 65 | 66 | exporter.shutdown() 67 | 68 | sourceSubscriber.request(1).expectComplete() 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/scala/au/com/titanclass/streams/telemetry/StreamSpanExporterTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Titan Class Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package au.com.titanclass.streams.telemetry 18 | 19 | import java.util 20 | 21 | import akka.actor.ActorSystem 22 | import akka.stream.testkit.scaladsl.TestSink 23 | import io.opentelemetry.common.{ Attributes, ReadableAttributes } 24 | import io.opentelemetry.sdk.common.InstrumentationLibraryInfo 25 | import io.opentelemetry.sdk.resources.Resource 26 | import io.opentelemetry.sdk.trace.data.SpanData 27 | import io.opentelemetry.trace.{ Span, SpanId, Status, TraceFlags, TraceId, TraceState } 28 | import utest._ 29 | 30 | import scala.concurrent.ExecutionContext 31 | import scala.jdk.CollectionConverters._ 32 | 33 | object StreamSpanExporterTest extends TestSuite { 34 | 35 | implicit lazy val system: ActorSystem = 36 | ActorSystem("tracing-reporter-tests") 37 | 38 | override def utestAfterAll(): Unit = 39 | system.terminate() 40 | 41 | implicit lazy val ec: ExecutionContext = 42 | system.dispatcher 43 | 44 | val tests: Tests = Tests { 45 | test("consume and shutdown") { 46 | val spanData = new SpanData() { 47 | override def getTraceId: TraceId = 48 | new TraceId(0, 0) 49 | 50 | override def getSpanId: SpanId = 51 | new SpanId(0) 52 | 53 | override def getTraceFlags: TraceFlags = 54 | TraceFlags.getDefault 55 | 56 | override def getTraceState: TraceState = 57 | TraceState.getDefault 58 | 59 | override def getParentSpanId: SpanId = 60 | SpanId.getInvalid 61 | 62 | override def getResource: Resource = 63 | Resource.getDefault 64 | 65 | override def getInstrumentationLibraryInfo: InstrumentationLibraryInfo = 66 | InstrumentationLibraryInfo.getEmpty 67 | 68 | override def getName: String = 69 | "name" 70 | 71 | override def getKind: Span.Kind = 72 | Span.Kind.INTERNAL 73 | 74 | override def getStartEpochNanos: Long = 75 | 0 76 | 77 | override def getAttributes: ReadableAttributes = 78 | Attributes.empty() 79 | 80 | override def getEvents: util.List[SpanData.Event] = 81 | List.empty.asJava 82 | 83 | override def getLinks: util.List[SpanData.Link] = 84 | List.empty.asJava 85 | 86 | override def getStatus: Status = 87 | Status.ABORTED 88 | 89 | override def getEndEpochNanos: Long = 90 | 0 91 | 92 | override def getHasRemoteParent: Boolean = 93 | false 94 | 95 | override def getHasEnded: Boolean = 96 | true 97 | 98 | override def getTotalRecordedEvents: Int = 99 | 0 100 | 101 | override def getTotalRecordedLinks: Int = 102 | 0 103 | 104 | override def getTotalAttributeCount: Int = 105 | 0 106 | } 107 | 108 | val spans = List(spanData).asJava 109 | 110 | val exporter = StreamSpanExporter(1) 111 | 112 | val sourceSubscriber = exporter.source.runWith(TestSink.probe[SpanData]) 113 | 114 | exporter.`export`(spans) 115 | 116 | sourceSubscriber.request(1).expectNext(spanData) 117 | 118 | exporter.shutdown() 119 | 120 | sourceSubscriber.request(1).expectComplete() 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/scala/au/com/titanclass/streams/telemetry/TracingProtobufMarshallingTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Titan Class Pty Ltd 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package au.com.titanclass.streams.telemetry 18 | 19 | import java.util 20 | 21 | import io.opentelemetry.common.{ Attributes, ReadableAttributes } 22 | import io.opentelemetry.sdk.common.InstrumentationLibraryInfo 23 | import io.opentelemetry.sdk.resources.Resource 24 | import io.opentelemetry.sdk.trace.data.SpanData 25 | import io.opentelemetry.trace.{ Span, SpanId, Status, TraceFlags, TraceId, TraceState } 26 | import utest._ 27 | 28 | import scala.jdk.CollectionConverters._ 29 | 30 | object TracingProtobufMarshallingTest extends TestSuite { 31 | 32 | val tests: Tests = Tests { 33 | test("write traces") { 34 | val spanData = new SpanData() { 35 | override def getTraceId: TraceId = 36 | new TraceId(0, 0) 37 | 38 | override def getSpanId: SpanId = 39 | new SpanId(0) 40 | 41 | override def getTraceFlags: TraceFlags = 42 | TraceFlags.getDefault 43 | 44 | override def getTraceState: TraceState = 45 | TraceState.getDefault 46 | 47 | override def getParentSpanId: SpanId = 48 | SpanId.getInvalid 49 | 50 | override def getResource: Resource = 51 | Resource.getDefault 52 | 53 | override def getInstrumentationLibraryInfo: InstrumentationLibraryInfo = 54 | InstrumentationLibraryInfo.getEmpty 55 | 56 | override def getName: String = 57 | "name" 58 | 59 | override def getKind: Span.Kind = 60 | Span.Kind.INTERNAL 61 | 62 | override def getStartEpochNanos: Long = 63 | 0 64 | 65 | override def getAttributes: ReadableAttributes = 66 | Attributes.empty() 67 | 68 | override def getEvents: util.List[SpanData.Event] = 69 | List.empty.asJava 70 | 71 | override def getLinks: util.List[SpanData.Link] = 72 | List.empty.asJava 73 | 74 | override def getStatus: Status = 75 | Status.ABORTED 76 | 77 | override def getEndEpochNanos: Long = 78 | 0 79 | 80 | override def getHasRemoteParent: Boolean = 81 | false 82 | 83 | override def getHasEnded: Boolean = 84 | true 85 | 86 | override def getTotalRecordedEvents: Int = 87 | 0 88 | 89 | override def getTotalRecordedLinks: Int = 90 | 0 91 | 92 | override def getTotalAttributeCount: Int = 93 | 0 94 | } 95 | 96 | import SpanProtobufMarshalling._ 97 | val span = spanData.toProtobuf.build() 98 | span.getName ==> "name" 99 | } 100 | } 101 | } 102 | --------------------------------------------------------------------------------