From 8e177b66d99c166244e1d831d41b14f54f6e73d3 Mon Sep 17 00:00:00 2001 From: Ivan Topolnjak Date: Mon, 11 Mar 2019 22:21:06 +0100 Subject: separate the status-page project from the status API --- .../main/vue/dist/favicon/android-chrome-96x96.png | Bin 5296 -> 0 bytes .../vue/dist/favicon/android-chrome-96x96.png~HEAD | Bin 0 -> 5296 bytes ...ate the status-page project from the status API | Bin 0 -> 5296 bytes .../src/main/vue/dist/favicon/apple-touch-icon.png | Bin 2422 -> 0 bytes .../vue/dist/favicon/apple-touch-icon.png~HEAD | Bin 0 -> 2422 bytes ...ate the status-page project from the status API | Bin 0 -> 2422 bytes .../src/main/vue/dist/favicon/browserconfig.xml | 9 ------- .../main/vue/dist/favicon/browserconfig.xml~HEAD | 9 +++++++ ...ate the status-page project from the status API | 9 +++++++ .../src/main/vue/dist/favicon/favicon-16x16.png | Bin 1151 -> 0 bytes .../main/vue/dist/favicon/favicon-16x16.png~HEAD | Bin 0 -> 1151 bytes ...ate the status-page project from the status API | Bin 0 -> 1151 bytes .../src/main/vue/dist/favicon/favicon-32x32.png | Bin 1556 -> 0 bytes .../main/vue/dist/favicon/favicon-32x32.png~HEAD | Bin 0 -> 1556 bytes ...ate the status-page project from the status API | Bin 0 -> 1556 bytes .../src/main/vue/dist/favicon/favicon.ico | Bin 15086 -> 0 bytes .../src/main/vue/dist/favicon/favicon.ico~HEAD | Bin 0 -> 15086 bytes ...ate the status-page project from the status API | Bin 0 -> 15086 bytes .../src/main/vue/dist/favicon/mstile-150x150.png | Bin 4918 -> 0 bytes .../main/vue/dist/favicon/mstile-150x150.png~HEAD | Bin 0 -> 4918 bytes ...ate the status-page project from the status API | Bin 0 -> 4918 bytes .../main/vue/dist/favicon/safari-pinned-tab.svg | 23 ------------------ .../vue/dist/favicon/safari-pinned-tab.svg~HEAD | 23 ++++++++++++++++++ ...ate the status-page project from the status API | 23 ++++++++++++++++++ .../src/main/vue/dist/favicon/site.webmanifest | 14 ----------- .../main/vue/dist/favicon/site.webmanifest~HEAD | 14 +++++++++++ ...ate the status-page project from the status API | 14 +++++++++++ .../src/main/vue/dist/img/logo.592500c9.svg | 26 --------------------- .../src/main/vue/dist/img/logo.592500c9.svg~HEAD | 26 +++++++++++++++++++++ ...ate the status-page project from the status API | 26 +++++++++++++++++++++ 30 files changed, 144 insertions(+), 72 deletions(-) delete mode 100644 kamon-status-page/src/main/vue/dist/favicon/android-chrome-96x96.png create mode 100644 kamon-status-page/src/main/vue/dist/favicon/android-chrome-96x96.png~HEAD create mode 100644 kamon-status-page/src/main/vue/dist/favicon/android-chrome-96x96.png~separate the status-page project from the status API delete mode 100644 kamon-status-page/src/main/vue/dist/favicon/apple-touch-icon.png create mode 100644 kamon-status-page/src/main/vue/dist/favicon/apple-touch-icon.png~HEAD create mode 100644 kamon-status-page/src/main/vue/dist/favicon/apple-touch-icon.png~separate the status-page project from the status API delete mode 100644 kamon-status-page/src/main/vue/dist/favicon/browserconfig.xml create mode 100644 kamon-status-page/src/main/vue/dist/favicon/browserconfig.xml~HEAD create mode 100644 kamon-status-page/src/main/vue/dist/favicon/browserconfig.xml~separate the status-page project from the status API delete mode 100644 kamon-status-page/src/main/vue/dist/favicon/favicon-16x16.png create mode 100644 kamon-status-page/src/main/vue/dist/favicon/favicon-16x16.png~HEAD create mode 100644 kamon-status-page/src/main/vue/dist/favicon/favicon-16x16.png~separate the status-page project from the status API delete mode 100644 kamon-status-page/src/main/vue/dist/favicon/favicon-32x32.png create mode 100644 kamon-status-page/src/main/vue/dist/favicon/favicon-32x32.png~HEAD create mode 100644 kamon-status-page/src/main/vue/dist/favicon/favicon-32x32.png~separate the status-page project from the status API delete mode 100644 kamon-status-page/src/main/vue/dist/favicon/favicon.ico create mode 100644 kamon-status-page/src/main/vue/dist/favicon/favicon.ico~HEAD create mode 100644 kamon-status-page/src/main/vue/dist/favicon/favicon.ico~separate the status-page project from the status API delete mode 100644 kamon-status-page/src/main/vue/dist/favicon/mstile-150x150.png create mode 100644 kamon-status-page/src/main/vue/dist/favicon/mstile-150x150.png~HEAD create mode 100644 kamon-status-page/src/main/vue/dist/favicon/mstile-150x150.png~separate the status-page project from the status API delete mode 100644 kamon-status-page/src/main/vue/dist/favicon/safari-pinned-tab.svg create mode 100644 kamon-status-page/src/main/vue/dist/favicon/safari-pinned-tab.svg~HEAD create mode 100644 kamon-status-page/src/main/vue/dist/favicon/safari-pinned-tab.svg~separate the status-page project from the status API delete mode 100644 kamon-status-page/src/main/vue/dist/favicon/site.webmanifest create mode 100644 kamon-status-page/src/main/vue/dist/favicon/site.webmanifest~HEAD create mode 100644 kamon-status-page/src/main/vue/dist/favicon/site.webmanifest~separate the status-page project from the status API delete mode 100644 kamon-status-page/src/main/vue/dist/img/logo.592500c9.svg create mode 100644 kamon-status-page/src/main/vue/dist/img/logo.592500c9.svg~HEAD create mode 100644 kamon-status-page/src/main/vue/dist/img/logo.592500c9.svg~separate the status-page project from the status API diff --git a/kamon-status-page/src/main/vue/dist/favicon/android-chrome-96x96.png b/kamon-status-page/src/main/vue/dist/favicon/android-chrome-96x96.png deleted file mode 100644 index 76bf6c17..00000000 Binary files a/kamon-status-page/src/main/vue/dist/favicon/android-chrome-96x96.png and /dev/null differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/android-chrome-96x96.png~HEAD b/kamon-status-page/src/main/vue/dist/favicon/android-chrome-96x96.png~HEAD new file mode 100644 index 00000000..76bf6c17 Binary files /dev/null and b/kamon-status-page/src/main/vue/dist/favicon/android-chrome-96x96.png~HEAD differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/android-chrome-96x96.png~separate the status-page project from the status API b/kamon-status-page/src/main/vue/dist/favicon/android-chrome-96x96.png~separate the status-page project from the status API new file mode 100644 index 00000000..76bf6c17 Binary files /dev/null and b/kamon-status-page/src/main/vue/dist/favicon/android-chrome-96x96.png~separate the status-page project from the status API differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/apple-touch-icon.png b/kamon-status-page/src/main/vue/dist/favicon/apple-touch-icon.png deleted file mode 100644 index 590c4c95..00000000 Binary files a/kamon-status-page/src/main/vue/dist/favicon/apple-touch-icon.png and /dev/null differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/apple-touch-icon.png~HEAD b/kamon-status-page/src/main/vue/dist/favicon/apple-touch-icon.png~HEAD new file mode 100644 index 00000000..590c4c95 Binary files /dev/null and b/kamon-status-page/src/main/vue/dist/favicon/apple-touch-icon.png~HEAD differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/apple-touch-icon.png~separate the status-page project from the status API b/kamon-status-page/src/main/vue/dist/favicon/apple-touch-icon.png~separate the status-page project from the status API new file mode 100644 index 00000000..590c4c95 Binary files /dev/null and b/kamon-status-page/src/main/vue/dist/favicon/apple-touch-icon.png~separate the status-page project from the status API differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/browserconfig.xml b/kamon-status-page/src/main/vue/dist/favicon/browserconfig.xml deleted file mode 100644 index 7738dd46..00000000 --- a/kamon-status-page/src/main/vue/dist/favicon/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #ffffff - - - diff --git a/kamon-status-page/src/main/vue/dist/favicon/browserconfig.xml~HEAD b/kamon-status-page/src/main/vue/dist/favicon/browserconfig.xml~HEAD new file mode 100644 index 00000000..7738dd46 --- /dev/null +++ b/kamon-status-page/src/main/vue/dist/favicon/browserconfig.xml~HEAD @@ -0,0 +1,9 @@ + + + + + + #ffffff + + + diff --git a/kamon-status-page/src/main/vue/dist/favicon/browserconfig.xml~separate the status-page project from the status API b/kamon-status-page/src/main/vue/dist/favicon/browserconfig.xml~separate the status-page project from the status API new file mode 100644 index 00000000..7738dd46 --- /dev/null +++ b/kamon-status-page/src/main/vue/dist/favicon/browserconfig.xml~separate the status-page project from the status API @@ -0,0 +1,9 @@ + + + + + + #ffffff + + + diff --git a/kamon-status-page/src/main/vue/dist/favicon/favicon-16x16.png b/kamon-status-page/src/main/vue/dist/favicon/favicon-16x16.png deleted file mode 100644 index 2058b6c0..00000000 Binary files a/kamon-status-page/src/main/vue/dist/favicon/favicon-16x16.png and /dev/null differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/favicon-16x16.png~HEAD b/kamon-status-page/src/main/vue/dist/favicon/favicon-16x16.png~HEAD new file mode 100644 index 00000000..2058b6c0 Binary files /dev/null and b/kamon-status-page/src/main/vue/dist/favicon/favicon-16x16.png~HEAD differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/favicon-16x16.png~separate the status-page project from the status API b/kamon-status-page/src/main/vue/dist/favicon/favicon-16x16.png~separate the status-page project from the status API new file mode 100644 index 00000000..2058b6c0 Binary files /dev/null and b/kamon-status-page/src/main/vue/dist/favicon/favicon-16x16.png~separate the status-page project from the status API differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/favicon-32x32.png b/kamon-status-page/src/main/vue/dist/favicon/favicon-32x32.png deleted file mode 100644 index dd4acaea..00000000 Binary files a/kamon-status-page/src/main/vue/dist/favicon/favicon-32x32.png and /dev/null differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/favicon-32x32.png~HEAD b/kamon-status-page/src/main/vue/dist/favicon/favicon-32x32.png~HEAD new file mode 100644 index 00000000..dd4acaea Binary files /dev/null and b/kamon-status-page/src/main/vue/dist/favicon/favicon-32x32.png~HEAD differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/favicon-32x32.png~separate the status-page project from the status API b/kamon-status-page/src/main/vue/dist/favicon/favicon-32x32.png~separate the status-page project from the status API new file mode 100644 index 00000000..dd4acaea Binary files /dev/null and b/kamon-status-page/src/main/vue/dist/favicon/favicon-32x32.png~separate the status-page project from the status API differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/favicon.ico b/kamon-status-page/src/main/vue/dist/favicon/favicon.ico deleted file mode 100644 index 6affcb49..00000000 Binary files a/kamon-status-page/src/main/vue/dist/favicon/favicon.ico and /dev/null differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/favicon.ico~HEAD b/kamon-status-page/src/main/vue/dist/favicon/favicon.ico~HEAD new file mode 100644 index 00000000..6affcb49 Binary files /dev/null and b/kamon-status-page/src/main/vue/dist/favicon/favicon.ico~HEAD differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/favicon.ico~separate the status-page project from the status API b/kamon-status-page/src/main/vue/dist/favicon/favicon.ico~separate the status-page project from the status API new file mode 100644 index 00000000..6affcb49 Binary files /dev/null and b/kamon-status-page/src/main/vue/dist/favicon/favicon.ico~separate the status-page project from the status API differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/mstile-150x150.png b/kamon-status-page/src/main/vue/dist/favicon/mstile-150x150.png deleted file mode 100644 index 74b62d82..00000000 Binary files a/kamon-status-page/src/main/vue/dist/favicon/mstile-150x150.png and /dev/null differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/mstile-150x150.png~HEAD b/kamon-status-page/src/main/vue/dist/favicon/mstile-150x150.png~HEAD new file mode 100644 index 00000000..74b62d82 Binary files /dev/null and b/kamon-status-page/src/main/vue/dist/favicon/mstile-150x150.png~HEAD differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/mstile-150x150.png~separate the status-page project from the status API b/kamon-status-page/src/main/vue/dist/favicon/mstile-150x150.png~separate the status-page project from the status API new file mode 100644 index 00000000..74b62d82 Binary files /dev/null and b/kamon-status-page/src/main/vue/dist/favicon/mstile-150x150.png~separate the status-page project from the status API differ diff --git a/kamon-status-page/src/main/vue/dist/favicon/safari-pinned-tab.svg b/kamon-status-page/src/main/vue/dist/favicon/safari-pinned-tab.svg deleted file mode 100644 index f031b484..00000000 --- a/kamon-status-page/src/main/vue/dist/favicon/safari-pinned-tab.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - - diff --git a/kamon-status-page/src/main/vue/dist/favicon/safari-pinned-tab.svg~HEAD b/kamon-status-page/src/main/vue/dist/favicon/safari-pinned-tab.svg~HEAD new file mode 100644 index 00000000..f031b484 --- /dev/null +++ b/kamon-status-page/src/main/vue/dist/favicon/safari-pinned-tab.svg~HEAD @@ -0,0 +1,23 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + diff --git a/kamon-status-page/src/main/vue/dist/favicon/safari-pinned-tab.svg~separate the status-page project from the status API b/kamon-status-page/src/main/vue/dist/favicon/safari-pinned-tab.svg~separate the status-page project from the status API new file mode 100644 index 00000000..f031b484 --- /dev/null +++ b/kamon-status-page/src/main/vue/dist/favicon/safari-pinned-tab.svg~separate the status-page project from the status API @@ -0,0 +1,23 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + diff --git a/kamon-status-page/src/main/vue/dist/favicon/site.webmanifest b/kamon-status-page/src/main/vue/dist/favicon/site.webmanifest deleted file mode 100644 index 2f99d71e..00000000 --- a/kamon-status-page/src/main/vue/dist/favicon/site.webmanifest +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "Kamon", - "short_name": "Kamon", - "icons": [ - { - "src": "/assets/favicon/android-chrome-96x96.png", - "sizes": "96x96", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/kamon-status-page/src/main/vue/dist/favicon/site.webmanifest~HEAD b/kamon-status-page/src/main/vue/dist/favicon/site.webmanifest~HEAD new file mode 100644 index 00000000..2f99d71e --- /dev/null +++ b/kamon-status-page/src/main/vue/dist/favicon/site.webmanifest~HEAD @@ -0,0 +1,14 @@ +{ + "name": "Kamon", + "short_name": "Kamon", + "icons": [ + { + "src": "/assets/favicon/android-chrome-96x96.png", + "sizes": "96x96", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/kamon-status-page/src/main/vue/dist/favicon/site.webmanifest~separate the status-page project from the status API b/kamon-status-page/src/main/vue/dist/favicon/site.webmanifest~separate the status-page project from the status API new file mode 100644 index 00000000..2f99d71e --- /dev/null +++ b/kamon-status-page/src/main/vue/dist/favicon/site.webmanifest~separate the status-page project from the status API @@ -0,0 +1,14 @@ +{ + "name": "Kamon", + "short_name": "Kamon", + "icons": [ + { + "src": "/assets/favicon/android-chrome-96x96.png", + "sizes": "96x96", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/kamon-status-page/src/main/vue/dist/img/logo.592500c9.svg b/kamon-status-page/src/main/vue/dist/img/logo.592500c9.svg deleted file mode 100644 index d351c48a..00000000 --- a/kamon-status-page/src/main/vue/dist/img/logo.592500c9.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/kamon-status-page/src/main/vue/dist/img/logo.592500c9.svg~HEAD b/kamon-status-page/src/main/vue/dist/img/logo.592500c9.svg~HEAD new file mode 100644 index 00000000..d351c48a --- /dev/null +++ b/kamon-status-page/src/main/vue/dist/img/logo.592500c9.svg~HEAD @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/kamon-status-page/src/main/vue/dist/img/logo.592500c9.svg~separate the status-page project from the status API b/kamon-status-page/src/main/vue/dist/img/logo.592500c9.svg~separate the status-page project from the status API new file mode 100644 index 00000000..d351c48a --- /dev/null +++ b/kamon-status-page/src/main/vue/dist/img/logo.592500c9.svg~separate the status-page project from the status API @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file -- cgit v1.2.3 From ff7c7b89335e3d3c463a57cd24321a1419a587ed Mon Sep 17 00:00:00 2001 From: Ivan Topolnjak Date: Sun, 24 Feb 2019 18:49:59 +0100 Subject: implement a generic abstraction for handling Tags --- .../src/test/scala/kamon/tag/TagsSpec.scala | 180 +++++++++++ kamon-core/src/main/scala/kamon/tag/Lookups.scala | 149 +++++++++ kamon-core/src/main/scala/kamon/tag/Tags.scala | 346 +++++++++++++++++++++ 3 files changed, 675 insertions(+) create mode 100644 kamon-core-tests/src/test/scala/kamon/tag/TagsSpec.scala create mode 100644 kamon-core/src/main/scala/kamon/tag/Lookups.scala create mode 100644 kamon-core/src/main/scala/kamon/tag/Tags.scala diff --git a/kamon-core-tests/src/test/scala/kamon/tag/TagsSpec.scala b/kamon-core-tests/src/test/scala/kamon/tag/TagsSpec.scala new file mode 100644 index 00000000..1a83e1c9 --- /dev/null +++ b/kamon-core-tests/src/test/scala/kamon/tag/TagsSpec.scala @@ -0,0 +1,180 @@ +package kamon.tag + +import java.util.Optional + +import org.scalatest.{Matchers, WordSpec} + +import scala.collection.JavaConverters.mapAsJavaMapConverter + +class TagsSpec extends WordSpec with Matchers { + import Lookups._ + + "Tags" should { + "silently drop null and unacceptable keys and/or values when constructed from the companion object builders" in { + Tags.from(NullString, NullString).all().size shouldBe 0 + Tags.from(EmptyString, NullString).all().size shouldBe 0 + Tags.from(EmptyString, "value").all().size shouldBe 0 + Tags.from(NullString, "value").all().size shouldBe 0 + Tags.from("key", NullString).all().size shouldBe 0 + Tags.from("key", NullBoolean).all().size shouldBe 0 + Tags.from("key", NullLong).all().size shouldBe 0 + + Tags.from(BadScalaTagMap).all().size shouldBe 0 + Tags.from(BadJavaTagMap).all().size shouldBe 0 + } + + "silently drop null keys and/or values when created with the .withTag, withTags or .and methods" in { + val tags = Tags.from("initialKey", "initialValue") + .withTag(NullString, NullString) + .withTag(EmptyString, NullString) + .withTag(EmptyString, "value") + .withTag(NullString, "value") + .withTag("key", NullString) + .withTag("key", NullBoolean) + .withTag("key", NullLong) + .and(NullString, NullString) + .and(EmptyString, NullString) + .and(EmptyString, "value") + .and(NullString, "value") + .and("key", NullString) + .and("key", NullBoolean) + .and("key", NullLong) + + tags.all().length shouldBe 1 + tags.all().head.asInstanceOf[Tag.String].key shouldBe "initialKey" + tags.all().head.asInstanceOf[Tag.String].value shouldBe "initialValue" + } + + "create a properly populated instance when valid pairs are provided" in { + Tags.from("isAwesome", true).all().size shouldBe 1 + Tags.from("name", "kamon").all().size shouldBe 1 + Tags.from("age", 5L).all().size shouldBe 1 + + Tags.from(GoodScalaTagMap).all().size shouldBe 3 + Tags.from(GoodJavaTagMap).all().size shouldBe 3 + + Tags.from("initial", "initial") + .withTag("isAwesome", true) + .withTag("name", "Kamon") + .withTag("age", 5L) + .and("isAvailable", true) + .and("website", "kamon.io") + .and("supportedPlatforms", 1L) + .all().size shouldBe 7 + } + + "override pre-existent tags when merging with other Tags instance" in { + val leftTags = Tags.from(GoodScalaTagMap) + val rightTags = Tags + .from("name", "New Kamon") + .and("age", 42L) + .and("isAwesome", false) // just for testing :) + + val tags = leftTags.withTags(rightTags) + tags.get(plain("name")) shouldBe "New Kamon" + tags.get(plainLong("age")) shouldBe 42L + tags.get(plainBoolean("isAwesome")) shouldBe false + + val andTags = tags and leftTags + andTags.get(plain("name")) shouldBe "Kamon" + andTags.get(plainLong("age")) shouldBe 5L + andTags.get(plainBoolean("isAwesome")) shouldBe true + } + + "provide typed access to the contained pairs when looking up values" in { + val tags = Tags.from(GoodScalaTagMap) + + tags.get(plain("name")) shouldBe "Kamon" + tags.get(plain("none")) shouldBe null + tags.get(option("name")) shouldBe Option("Kamon") + tags.get(option("none")) shouldBe None + tags.get(optional("name")) shouldBe Optional.of("Kamon") + tags.get(optional("none")) shouldBe Optional.empty() + + tags.get(plainLong("age")) shouldBe 5L + tags.get(plainLong("nil")) shouldBe null + tags.get(longOption("age")) shouldBe Option(5L) + tags.get(longOption("nil")) shouldBe None + tags.get(longOptional("age")) shouldBe Optional.of(5L) + tags.get(longOptional("nil")) shouldBe Optional.empty() + + tags.get(plainBoolean("isAwesome")) shouldBe true + tags.get(plainBoolean("isUnknown")) shouldBe null + tags.get(booleanOption("isAwesome")) shouldBe Some(true) + tags.get(booleanOption("isUnknown")) shouldBe None + tags.get(booleanOptional("isAwesome")) shouldBe Optional.of(true) + tags.get(booleanOptional("isUnknown")) shouldBe Optional.empty() + + tags.get(coerce("age")) shouldBe "5" + tags.get(coerce("isAwesome")) shouldBe "true" + tags.get(coerce("unknown")) shouldBe "unknown" + } + + "allow iterating over all contained tags" in { + val tags = Tags.from(Map( + "age" -> 5L, + "name" -> "Kamon", + "isAwesome" -> true, + "hasTracing" -> true, + "website" -> "kamon.io", + "luckyNumber" -> 7L + )) + + tags.iterator().length shouldBe 6 + tags.iterator().find(matchPair("age", 5L)) shouldBe defined + tags.iterator().find(matchPair("luckyNumber", 7L)) shouldBe defined + tags.iterator().find(matchPair("hasTracing", true)) shouldBe defined + tags.iterator().find(matchPair("isAwesome", true)) shouldBe defined + tags.iterator().find(matchPair("website", "kamon.io")) shouldBe defined + tags.iterator().find(matchPair("name", "Kamon")) shouldBe defined + } + + "be equal to other Tags instance with the same tags" in { + Tags.from(GoodScalaTagMap) shouldBe Tags.from(GoodScalaTagMap) + Tags.from(GoodJavaTagMap) shouldBe Tags.from(GoodJavaTagMap) + } + + "have a readable toString implementation" in { + Tags.from(GoodScalaTagMap).toString() should include("age=5") + Tags.from(GoodScalaTagMap).toString() should include("name=Kamon") + Tags.from(GoodScalaTagMap).toString() should include("isAwesome=true") + } + } + + def matchPair(key: String, value: Any) = { tag: Tag => { + tag match { + case t: Tag.String => t.key == key && t.value == value + case t: Tag.Long => t.key == key && t.value == value + case t: Tag.Boolean => t.key == key && t.value == value + } + + }} + + + val NullString: java.lang.String = null + val NullBoolean: java.lang.Boolean = NullString.asInstanceOf[java.lang.Boolean] + val NullLong: java.lang.Long = null + val EmptyString: java.lang.String = "" + + val GoodScalaTagMap: Map[String, Any] = Map( + "age" -> 5L, + "name" -> "Kamon", + "isAwesome" -> true + ) + + val BadScalaTagMap: Map[String, Any] = Map( + NullString -> NullString, + EmptyString -> NullString, + NullString -> NullString, + EmptyString -> NullString, + EmptyString -> "value", + NullString -> "value", + "key" -> NullString, + "key" -> NullBoolean, + "key" -> NullLong + ) + + val GoodJavaTagMap = GoodScalaTagMap.asJava + val BadJavaTagMap = BadScalaTagMap.asJava + +} diff --git a/kamon-core/src/main/scala/kamon/tag/Lookups.scala b/kamon-core/src/main/scala/kamon/tag/Lookups.scala new file mode 100644 index 00000000..44ffb6f4 --- /dev/null +++ b/kamon-core/src/main/scala/kamon/tag/Lookups.scala @@ -0,0 +1,149 @@ +package kamon.tag + +import java.util.Optional +import java.lang.{Boolean => JBoolean, Long => JLong, String => JString} + +import kamon.tag.Tags.Lookup + +import scala.reflect.ClassTag + +object Lookups { + + /** + * Finds a String value associated to the provided key and returns it. If the key is not present or the value + * associated with they is not a String then a null is returned. + */ + def plain(key: JString) = new Lookup[JString] { + override def run(storage: Map[JString, Any]): JString = + findAndTransform(key, storage, _plainString, null) + } + + + /** + * Finds a String value associated to the provided key and returns it, wrapped in an Option[String]. If the key is + * not present or the value associated with they is not a String then a None is returned. + */ + def option(key: JString) = new Lookup[Option[JString]] { + override def run(storage: Map[JString, Any]): Option[JString] = + findAndTransform(key, storage, _stringOption, None) + } + + + /** + * Finds a String value associated to the provided key and returns it, wrapped in an Optional[String]. If the key + * is not present or the value associated with they is not a String then Optional.empty() is returned. + */ + def optional(key: JString) = new Lookup[Optional[String]] { + override def run(storage: Map[String, Any]): Optional[String] = + findAndTransform(key, storage, _stringOptional, Optional.empty()) + } + + + /** + * Finds the value associated to the provided key and coerces it to a String representation. If the key is not + * present then "unknown" (as a String) will be returned. If the value associated with the key is not a String then + * the value of the key will be transformed into a String and returned. + * + * This lookup type is guaranteed to return a non-null String representation of value. + */ + def coerce(key: String) = new Lookup[String] { + override def run(storage: Map[String, Any]): String = { + val value = storage(key) + if(value == null) + "unknown" + else + value.toString + } + } + + + /** + * Finds a Boolean value associated to the provided key and returns it. If the key is not present or the value + * associated with they is not a Boolean then a null is returned. + */ + def plainBoolean(key: String) = new Lookup[JBoolean] { + override def run(storage: Map[String, Any]): JBoolean = + findAndTransform(key, storage, _plainBoolean, null) + } + + + /** + * Finds a Boolean value associated to the provided key and returns it, wrapped in an Option[Boolean]. If the key + * is not present or the value associated with they is not a Boolean then a None is returned. + */ + def booleanOption(key: String) = new Lookup[Option[JBoolean]] { + override def run(storage: Map[String, Any]): Option[JBoolean] = + findAndTransform(key, storage, _booleanOption, None) + } + + + /** + * Finds a Boolean value associated to the provided key and returns it, wrapped in an Optional[Boolean]. If the key + * is not present or the value associated with they is not a Boolean then Optional.empty() is returned. + */ + def booleanOptional(key: String) = new Lookup[Optional[JBoolean]] { + override def run(storage: Map[String, Any]): Optional[JBoolean] = + findAndTransform(key, storage, _booleanOptional, Optional.empty()) + } + + + /** + * Finds a Long value associated to the provided key and returns it. If the key is not present or the value + * associated with they is not a Long then a null is returned. + */ + def plainLong(key: String) = new Lookup[JLong] { + override def run(storage: Map[String, Any]): JLong = + findAndTransform(key, storage, _plainLong, null) + } + + + /** + * Finds a Long value associated to the provided key and returns it, wrapped in an Option[Long]. If the key is + * not present or the value associated with they is not a Long then a None is returned. + */ + def longOption(key: String) = new Lookup[Option[JLong]] { + override def run(storage: Map[String, Any]): Option[JLong] = + findAndTransform(key, storage, _longOption, None) + } + + + /** + * Finds a Long value associated to the provided key and returns it, wrapped in an Optional[Long]. If the key + * is not present or the value associated with they is not a Long then Optional.empty() is returned. + */ + def longOptional(key: String) = new Lookup[Optional[JLong]] { + override def run(storage: Map[String, Any]): Optional[JLong] = + findAndTransform(key, storage, _longOptional, Optional.empty()) + } + + + //////////////////////////////////////////////////////////////// + // Transformation helpers for the lookup DSL // + //////////////////////////////////////////////////////////////// + + private def findAndTransform[T, R](key: String, storage: Map[String, Any], transform: R => T, default: T) + (implicit ct: ClassTag[R]): T = { + + // This assumes that this code will only be used to lookup values from a Tags instance + // for which the underlying map always has "null" as the default value. + val value = storage(key) + + if(value == null || !ct.runtimeClass.isInstance(value)) + default + else + transform(value.asInstanceOf[R]) + } + + private val _plainString = (a: JString) => a + private val _stringOption = (a: JString) => Option(a) + private val _stringOptional = (a: JString) => Optional.of(a) + + private val _plainLong = (a: JLong) => a + private val _longOption = (a: JLong) => Option(a) + private val _longOptional = (a: JLong) => Optional.of(a) + + private val _plainBoolean = (a: JBoolean) => a + private val _booleanOption = (a: JBoolean) => Option(a) + private val _booleanOptional = (a: JBoolean) => Optional.of(a) + +} diff --git a/kamon-core/src/main/scala/kamon/tag/Tags.scala b/kamon-core/src/main/scala/kamon/tag/Tags.scala new file mode 100644 index 00000000..b7813da6 --- /dev/null +++ b/kamon-core/src/main/scala/kamon/tag/Tags.scala @@ -0,0 +1,346 @@ +package kamon.tag + +import kamon.tag.Tags.Lookup + +import scala.collection.JavaConverters.asScalaIteratorConverter +import java.lang.{Boolean => JBoolean, Long => JLong, String => JString} + +import org.slf4j.LoggerFactory + + +/** + * Marker trait for allowed Tag implementations. Users are not meant to create implementations of this trait outside + * of Kamon. + */ +sealed trait Tag + +object Tag { + + /** + * Represents a String key pointing to a String value. + */ + trait String extends Tag { + def key: JString + def value: JString + } + + /** + * Represents a String key pointing to a Boolean value. + */ + trait Boolean extends Tag { + def key: JString + def value: JBoolean + } + + /** + * Represents a String key pointing to a Long value. + */ + trait Long extends Tag { + def key: JString + def value: JLong + } +} + + +/** + * A immutable collection of key/value pairs with specialized support for storing String keys pointing to String, Long + * and/or Boolean values. + * + * Instances of Tags store all pairs in the same data structure, but preserving type information for the stored pairs + * and providing a simple DSL for accessing those values and expressing type expectations. It is also possible to + * lookup pairs without prescribing a mechanism for handling missing values. I.e. users of this class can decide + * whether to receive a null, java.util.Optional, scala.Option or any other value when looking up a pair. + * + * Tags can only be created from the builder functions on the Tags companion object. There are two different options + * to read the contained pairs from a Tags instance: + * + * 1. Using the lookup DSL. You can use the Lookup DSL when you know exactly that you are trying to get out of the + * tags instance. The lookup DSL is biased towards String keys since they are by far the most common case. For + * example, to get a given tag as an Option[String] and another as an Option[Boolean] the following code should + * suffice: + * + * import kamon.tag.Tags.Lookup._ + * val tags = Tags.from(tagMap) + * val name = tags.get(option("name")) + * val isSignedIn = tags.get(booleanOption("isSignedIn")) + * + * 2. Using the .all() and .iterator variants. This option requires you to test the returned instances to verify + * whether they are a Tag.String, Tag.Long or Tag.Boolean instance and act accordingly. Fortunately this + * cumbersome operation is rarely necessary on user-facing code. + * + */ +class Tags private(private val _tags: Map[String, Any]) { + import Tags.withPair + + /** + * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated + * with another value then the previous value will be discarded and overwritten with the provided one. + */ + def withTag(key: String, value: JString): Tags = + withPair(this, key, value) + + + /** + * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated + * with another value then the previous value will be discarded and overwritten with the provided one. + */ + def withTag(key: String, value: JBoolean): Tags = + withPair(this, key, value) + + + /** + * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated + * with another value then the previous value will be discarded and overwritten with the provided one. + */ + def withTag(key: String, value: JLong): Tags = + withPair(this, key, value) + + + /** + * Creates a new Tags instance that includes all the tags from the provided Tags instance. If any of the tags in this + * instance are associated to a key present on the provided instance then the previous value will be discarded and + * overwritten with the provided one. + */ + def withTags(other: Tags): Tags = + new Tags(_tags ++ other._tags) + + + /** + * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated + * with another value then the previous value will be discarded and overwritten with the provided one. + */ + def and(key: String, value: JString): Tags = + withPair(this, key, value) + + + /** + * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated + * with another value then the previous value will be discarded and overwritten with the provided one. + */ + def and(key: String, value: JBoolean): Tags = + withPair(this, key, value) + + + /** + * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated + * with another value then the previous value will be discarded and overwritten with the provided one. + */ + def and(key: String, value: JLong): Tags = + withPair(this, key, value) + + + /** + * Creates a new Tags instance that includes all the tags from the provided Tags instance. If any of the tags in this + * instance are associated to a key present on the provided instance then the previous value will be discarded and + * overwritten with the provided one. + */ + def and(other: Tags): Tags = + new Tags(_tags ++ other._tags) + + /** + * Executes a tag lookup on the instance. The return type of this function will depend on the provided Lookup + * instance. Take a look at the built-in lookups on the Tags.Lookup companion object for more information. + */ + def get[T](lookup: Lookup[T]): T = + lookup.run(_tags) + + /** + * Returns a immutable sequence of tags created from the contained tags internal representation. Calling this method + * will cause the creation of a new data structure. Unless you really need to have all the tags as immutable + * instances it is recommended to use the .iterator() function instead. + * + * The returned sequence contains immutable values and is safe to share across threads. + */ + def all(): Seq[Tag] = + _tags.foldLeft(List.empty[Tag]) { + case (ts, (key, value)) => value match { + case v: String => new immutable.String(key, v) :: ts + case v: Boolean => new immutable.Boolean(key, v) :: ts + case v: Long => new immutable.Long(key, v) :: ts + } + } + + /** + * Returns an iterator of tags. The underlying iterator reuses the Tag instances to avoid unnecessary intermediate + * allocations and thus, it is not safe to share across threads. The most common case for tags iterators is on + * reporters which will need to iterate through all existent tags only to copy their values into a separate data + * structure that will be sent to the external systems. + */ + def iterator(): Iterator[Tag] = new Iterator[Tag] { + private val _entriesIterator = _tags.iterator + private var _longTag: mutable.Long = null + private var _stringTag: mutable.String = null + private var _booleanTag: mutable.Boolean = null + + override def hasNext: Boolean = + _entriesIterator.hasNext + + override def next(): Tag = { + val (key, value) = _entriesIterator.next() + value match { + case v: String => stringTag(key, v) + case v: Boolean => booleanTag(key, v) + case v: Long => longTag(key, v) + } + } + + private def stringTag(key: JString, value: JString): Tag.String = + if(_stringTag == null) { + _stringTag = new mutable.String(key, value) + _stringTag + } else _stringTag.updated(key, value) + + private def booleanTag(key: JString, value: JBoolean): Tag.Boolean = + if(_booleanTag == null) { + _booleanTag = new mutable.Boolean(key, value) + _booleanTag + } else _booleanTag.updated(key, value) + + private def longTag(key: JString, value: JLong): Tag.Long = + if(_longTag == null) { + _longTag = new mutable.Long(key, value) + _longTag + } else _longTag.updated(key, value) + } + + + override def equals(other: Any): Boolean = + other != null && other.isInstanceOf[Tags] && other.asInstanceOf[Tags]._tags == this._tags + + + override def toString: JString = { + val sb = new StringBuilder() + sb.append("Tags{") + + var hasTags = false + _tags.foreach { case (k, v) => + if(hasTags) + sb.append(",") + + sb.append(k) + .append("=") + .append(v) + + hasTags = true + } + + sb.append("}").toString() + } + + private object immutable { + class String(val key: JString, val value: JString) extends Tag.String + class Boolean(val key: JString, val value: JBoolean) extends Tag.Boolean + class Long(val key: JString, val value: JLong) extends Tag.Long + } + + private object mutable { + class String(var key: JString, var value: JString) extends Tag.String with Updateable[JString] + class Boolean(var key: JString, var value: JBoolean) extends Tag.Boolean with Updateable[JBoolean] + class Long(var key: JString, var value: JLong) extends Tag.Long with Updateable[JLong] + + trait Updateable[T] { + var key: JString + var value: T + + def updated(key: JString, value: T): this.type = { + this.key = key + this.value = value + this + } + } + } +} + +object Tags { + + /** + * A valid instance of tags that doesn't contain any pairs. + */ + val Empty = new Tags(Map.empty.withDefaultValue(null)) + + + /** + * Construct a new Tags instance with a single key/value pair. + */ + def from(key: String, value: JString): Tags = + withPair(Empty, key, value) + + + /** + * Construct a new Tags instance with a single key/value pair. + */ + def from(key: String, value: JBoolean): Tags = + withPair(Empty, key, value) + + + /** + * Construct a new Tags instance with a single key/value pair. + */ + def from(key: String, value: JLong): Tags = + withPair(Empty, key, value) + + + /** + * Constructs a new Tags instance from a Map. The returned Tags will only contain the entries that have String, Long + * or Boolean values from the supplied map, any other entry in the map will be ignored. + */ + def from(map: Map[String, Any]): Tags = + new Tags(map.filter { case (k, v) => isValidPair(k, v) } withDefaultValue(null)) + + + /** + * Constructs a new Tags instance from a Map. The returned Tags will only contain the entries that have String, Long + * or Boolean values from the supplied map, any other entry in the map will be ignored. + */ + def from(map: java.util.Map[String, Any]): Tags = { + val allowedTags = Map.newBuilder[String, Any] + map.entrySet() + .iterator() + .asScala + .foreach(e => if(isValidPair(e.getKey, e.getValue)) allowedTags += (e.getKey -> e.getValue)) + + new Tags(allowedTags.result().withDefaultValue(null)) + } + + + private val _logger = LoggerFactory.getLogger(classOf[Tags]) + + private def withPair(parent: Tags, key: String, value: Any): Tags = + if(isValidPair(key, value)) + new Tags(parent._tags.updated(key, value)) + else + parent + + private def isValidPair(key: String, value: Any): Boolean = { + val isValidKey = key != null && key.nonEmpty + val isValidValue = isAllowedTagValue(value) + val isValid = isValidKey && isValidValue + + if(!isValid && _logger.isDebugEnabled) { + if(!isValidKey && !isValidValue) + _logger.debug(s"Dismissing tag with invalid key [$key] and invalid value [$value]") + else if(!isValidKey) + _logger.debug(s"Dismissing tag with invalid key [$key] and value [$value]") + else + _logger.debug(s"Dismissing tag with key [$key] and invalid value [$value]") + } + + isValid + } + + private def isAllowedTagValue(v: Any): Boolean = + v != null && (v.isInstanceOf[String] || v.isInstanceOf[Boolean] || v.isInstanceOf[Long]) + + + /** + * Describes a strategy to lookup values from a Tags instance. Implementations of this interface will be provided + * with the actual data structure containing the tags and must perform any necessary runtime type checks to ensure + * that the returned value is in assignable to the expected type T. + * + * Several implementation are provided in the Lookup companion object and it is recommended to import and use those + * definitions when looking up keys from a Tags instance. + */ + trait Lookup[T] { + def run(storage: Map[String, Any]): T + } +} \ No newline at end of file -- cgit v1.2.3 From 91575b8db60ca4fc6df9f44fa720929d8c3868ac Mon Sep 17 00:00:00 2001 From: Ivan Topolnjak Date: Mon, 25 Feb 2019 00:21:40 +0100 Subject: use TagSet as the implementation for Context tags --- .../kamon/context/BinaryPropagationSpec.scala | 12 +- .../scala/kamon/context/HttpPropagationSpec.scala | 21 +- .../HttpServerInstrumentationSpec.scala | 13 +- .../src/test/scala/kamon/tag/TagSetSpec.scala | 180 ++++++++ .../src/test/scala/kamon/tag/TagsSpec.scala | 180 -------- kamon-core/src/main/colfer/Context.colf | 27 +- .../generated/binary/context/BooleanTag.java | 412 +++++++++++++++++ .../context/generated/binary/context/Context.java | 107 +---- .../context/generated/binary/context/Entry.java | 76 +-- .../context/generated/binary/context/LongTag.java | 443 ++++++++++++++++++ .../generated/binary/context/StringTag.java | 466 +++++++++++++++++++ .../context/generated/binary/context/Tags.java | 513 +++++++++++++++++++++ .../scala/kamon/context/BinaryPropagation.scala | 81 ++-- .../src/main/scala/kamon/context/Context.scala | 126 ++++- .../main/scala/kamon/context/HttpPropagation.scala | 65 ++- .../scala/kamon/instrumentation/HttpServer.scala | 24 +- .../src/main/scala/kamon/metric/Accumulator.scala | 6 +- .../src/main/scala/kamon/metric/Metric.scala | 24 +- .../main/scala/kamon/metric/PeriodSnapshot.scala | 4 +- kamon-core/src/main/scala/kamon/metric/Timer.scala | 6 +- kamon-core/src/main/scala/kamon/package.scala | 2 +- kamon-core/src/main/scala/kamon/tag/Lookups.scala | 32 +- kamon-core/src/main/scala/kamon/tag/Tag.scala | 50 ++ kamon-core/src/main/scala/kamon/tag/TagSet.scala | 328 +++++++++++++ kamon-core/src/main/scala/kamon/tag/Tags.scala | 346 -------------- 25 files changed, 2751 insertions(+), 793 deletions(-) create mode 100644 kamon-core-tests/src/test/scala/kamon/tag/TagSetSpec.scala delete mode 100644 kamon-core-tests/src/test/scala/kamon/tag/TagsSpec.scala create mode 100644 kamon-core/src/main/java/kamon/context/generated/binary/context/BooleanTag.java create mode 100644 kamon-core/src/main/java/kamon/context/generated/binary/context/LongTag.java create mode 100644 kamon-core/src/main/java/kamon/context/generated/binary/context/StringTag.java create mode 100644 kamon-core/src/main/java/kamon/context/generated/binary/context/Tags.java create mode 100644 kamon-core/src/main/scala/kamon/tag/Tag.scala create mode 100644 kamon-core/src/main/scala/kamon/tag/TagSet.scala delete mode 100644 kamon-core/src/main/scala/kamon/tag/Tags.scala diff --git a/kamon-core-tests/src/test/scala/kamon/context/BinaryPropagationSpec.scala b/kamon-core-tests/src/test/scala/kamon/context/BinaryPropagationSpec.scala index 5681d300..4fa7116d 100644 --- a/kamon-core-tests/src/test/scala/kamon/context/BinaryPropagationSpec.scala +++ b/kamon-core-tests/src/test/scala/kamon/context/BinaryPropagationSpec.scala @@ -6,6 +6,8 @@ import com.typesafe.config.ConfigFactory import kamon.Kamon import kamon.context.BinaryPropagation.{ByteStreamReader, ByteStreamWriter} import kamon.context.Propagation.{EntryReader, EntryWriter} +import kamon.tag.TagSet +import kamon.tag.Lookups._ import org.scalatest.{Matchers, OptionValues, WordSpec} import scala.util.Random @@ -70,13 +72,14 @@ class BinaryPropagationSpec extends WordSpec with Matchers with OptionValues { } "round trip a Context that only has tags" in { - val context = Context.of(Map("hello" -> "world", "kamon" -> "rulez")) + val context = Context.of(TagSet.from(Map("hello" -> "world", "kamon" -> "rulez"))) val writer = inspectableByteStreamWriter() binaryPropagation.write(context, writer) val rtContext = binaryPropagation.read(ByteStreamReader.of(writer.toByteArray)) rtContext.entries shouldBe empty - rtContext.tags should contain theSameElementsAs (context.tags) + rtContext.tags.get(plain("hello")) shouldBe "world" + rtContext.tags.get(plain("kamon")) shouldBe "rulez" } "round trip a Context that only has entries" in { @@ -91,7 +94,7 @@ class BinaryPropagationSpec extends WordSpec with Matchers with OptionValues { } "round trip a Context that with tags and entries" in { - val context = Context.of(Map("hello" -> "world", "kamon" -> "rulez")) + val context = Context.of(TagSet.from(Map("hello" -> "world", "kamon" -> "rulez"))) .withKey(BinaryPropagationSpec.StringKey, "string-value") .withKey(BinaryPropagationSpec.IntegerKey, 42) @@ -99,7 +102,8 @@ class BinaryPropagationSpec extends WordSpec with Matchers with OptionValues { binaryPropagation.write(context, writer) val rtContext = binaryPropagation.read(ByteStreamReader.of(writer.toByteArray)) - rtContext.tags should contain theSameElementsAs (context.tags) + rtContext.tags.get(plain("hello")) shouldBe "world" + rtContext.tags.get(plain("kamon")) shouldBe "rulez" rtContext.get(BinaryPropagationSpec.StringKey) shouldBe "string-value" rtContext.get(BinaryPropagationSpec.IntegerKey) shouldBe 0 // there is no entry configuration for the integer key } diff --git a/kamon-core-tests/src/test/scala/kamon/context/HttpPropagationSpec.scala b/kamon-core-tests/src/test/scala/kamon/context/HttpPropagationSpec.scala index fcddfe24..0cd10672 100644 --- a/kamon-core-tests/src/test/scala/kamon/context/HttpPropagationSpec.scala +++ b/kamon-core-tests/src/test/scala/kamon/context/HttpPropagationSpec.scala @@ -5,6 +5,8 @@ import kamon.Kamon import kamon.context.HttpPropagation.{HeaderReader, HeaderWriter} import kamon.context.Propagation.{EntryReader, EntryWriter} import org.scalatest.{Matchers, OptionValues, WordSpec} +import kamon.tag.Lookups._ +import kamon.tag.TagSet import scala.collection.mutable @@ -22,12 +24,11 @@ class HttpPropagationSpec extends WordSpec with Matchers with OptionValues { "x-content-tags" -> "hello=world;correlation=1234", "x-mapped-tag" -> "value" ) + val context = httpPropagation.read(headerReaderFromMap(headers)) - context.tags should contain only( - "hello" -> "world", - "correlation" -> "1234", - "mappedTag" -> "value" - ) + context.tags.get(plain("hello")) shouldBe "world" + context.tags.get(plain("correlation")) shouldBe "1234" + context.tags.get(plain("mappedTag")) shouldBe "value" } "handle errors when reading HTTP headers" in { @@ -48,9 +49,9 @@ class HttpPropagationSpec extends WordSpec with Matchers with OptionValues { context.get(HttpPropagationSpec.StringKey) shouldBe "hey" context.get(HttpPropagationSpec.IntegerKey) shouldBe 123 context.get(HttpPropagationSpec.OptionalKey) shouldBe empty - context.getTag("hello").value shouldBe "world" - context.getTag("correlation").value shouldBe "1234" - context.getTag("unknown") shouldBe empty + context.getTag(plain("hello")) shouldBe "world" + context.getTag(option("correlation")).value shouldBe "1234" + context.getTag(option("unknown")) shouldBe empty } } @@ -64,10 +65,10 @@ class HttpPropagationSpec extends WordSpec with Matchers with OptionValues { "write context tags when available" in { val headers = mutable.Map.empty[String, String] - val context = Context.of(Map( + val context = Context.of(TagSet.from(Map( "hello" -> "world", "mappedTag" -> "value" - )) + ))) httpPropagation.write(context, headerWriterFromMap(headers)) headers should contain only( diff --git a/kamon-core-tests/src/test/scala/kamon/instrumentation/HttpServerInstrumentationSpec.scala b/kamon-core-tests/src/test/scala/kamon/instrumentation/HttpServerInstrumentationSpec.scala index 62eae45b..c7e856d0 100644 --- a/kamon-core-tests/src/test/scala/kamon/instrumentation/HttpServerInstrumentationSpec.scala +++ b/kamon-core-tests/src/test/scala/kamon/instrumentation/HttpServerInstrumentationSpec.scala @@ -4,6 +4,7 @@ import java.time.Duration import kamon.context.Context import kamon.metric.{Counter, Histogram, RangeSampler} +import kamon.tag.Lookups._ import kamon.testkit.{MetricInspection, SpanInspection} import org.scalatest.concurrent.Eventually import org.scalatest.{Matchers, OptionValues, WordSpec} @@ -20,10 +21,8 @@ class HttpServerInstrumentationSpec extends WordSpec with Matchers with SpanInsp "custom-trace-id" -> "0011223344556677" ))) - handler.context.tags should contain only( - "tag" -> "value", - "none" -> "0011223344556677" - ) + handler.context.tags.get(plain("tag")) shouldBe "value" + handler.context.tags.get(plain("none")) shouldBe "0011223344556677" handler.send(fakeResponse(200, mutable.Map.empty), Context.Empty) handler.doneSending(0L) @@ -35,10 +34,8 @@ class HttpServerInstrumentationSpec extends WordSpec with Matchers with SpanInsp "custom-trace-id" -> "0011223344556677" ))) - handler.context.tags should contain only( - "tag" -> "value", - "none" -> "0011223344556677" - ) + handler.context.tags.get(plain("tag")) shouldBe "value" + handler.context.tags.get(plain("none")) shouldBe "0011223344556677" val span = inspect(handler.span) span.context().traceID.string shouldNot be("0011223344556677") diff --git a/kamon-core-tests/src/test/scala/kamon/tag/TagSetSpec.scala b/kamon-core-tests/src/test/scala/kamon/tag/TagSetSpec.scala new file mode 100644 index 00000000..cd23c58d --- /dev/null +++ b/kamon-core-tests/src/test/scala/kamon/tag/TagSetSpec.scala @@ -0,0 +1,180 @@ +package kamon.tag + +import java.util.Optional + +import org.scalatest.{Matchers, WordSpec} + +import scala.collection.JavaConverters.mapAsJavaMapConverter + +class TagSetSpec extends WordSpec with Matchers { + import Lookups._ + + "Tags" should { + "silently drop null and unacceptable keys and/or values when constructed from the companion object builders" in { + TagSet.from(NullString, NullString).all().size shouldBe 0 + TagSet.from(EmptyString, NullString).all().size shouldBe 0 + TagSet.from(EmptyString, "value").all().size shouldBe 0 + TagSet.from(NullString, "value").all().size shouldBe 0 + TagSet.from("key", NullString).all().size shouldBe 0 + TagSet.from("key", NullBoolean).all().size shouldBe 0 + TagSet.from("key", NullLong).all().size shouldBe 0 + + TagSet.from(BadScalaTagMap).all().size shouldBe 0 + TagSet.from(BadJavaTagMap).all().size shouldBe 0 + } + + "silently drop null keys and/or values when created with the .withTag, withTags or .and methods" in { + val tags = TagSet.from("initialKey", "initialValue") + .withTag(NullString, NullString) + .withTag(EmptyString, NullString) + .withTag(EmptyString, "value") + .withTag(NullString, "value") + .withTag("key", NullString) + .withTag("key", NullBoolean) + .withTag("key", NullLong) + .and(NullString, NullString) + .and(EmptyString, NullString) + .and(EmptyString, "value") + .and(NullString, "value") + .and("key", NullString) + .and("key", NullBoolean) + .and("key", NullLong) + + tags.all().length shouldBe 1 + tags.all().head.asInstanceOf[Tag.String].key shouldBe "initialKey" + tags.all().head.asInstanceOf[Tag.String].value shouldBe "initialValue" + } + + "create a properly populated instance when valid pairs are provided" in { + TagSet.from("isAwesome", true).all().size shouldBe 1 + TagSet.from("name", "kamon").all().size shouldBe 1 + TagSet.from("age", 5L).all().size shouldBe 1 + + TagSet.from(GoodScalaTagMap).all().size shouldBe 3 + TagSet.from(GoodJavaTagMap).all().size shouldBe 3 + + TagSet.from("initial", "initial") + .withTag("isAwesome", true) + .withTag("name", "Kamon") + .withTag("age", 5L) + .and("isAvailable", true) + .and("website", "kamon.io") + .and("supportedPlatforms", 1L) + .all().size shouldBe 7 + } + + "override pre-existent tags when merging with other Tags instance" in { + val leftTags = TagSet.from(GoodScalaTagMap) + val rightTags = TagSet + .from("name", "New Kamon") + .and("age", 42L) + .and("isAwesome", false) // just for testing :) + + val tags = leftTags.withTags(rightTags) + tags.get(plain("name")) shouldBe "New Kamon" + tags.get(plainLong("age")) shouldBe 42L + tags.get(plainBoolean("isAwesome")) shouldBe false + + val andTags = tags and leftTags + andTags.get(plain("name")) shouldBe "Kamon" + andTags.get(plainLong("age")) shouldBe 5L + andTags.get(plainBoolean("isAwesome")) shouldBe true + } + + "provide typed access to the contained pairs when looking up values" in { + val tags = TagSet.from(GoodScalaTagMap) + + tags.get(plain("name")) shouldBe "Kamon" + tags.get(plain("none")) shouldBe null + tags.get(option("name")) shouldBe Option("Kamon") + tags.get(option("none")) shouldBe None + tags.get(optional("name")) shouldBe Optional.of("Kamon") + tags.get(optional("none")) shouldBe Optional.empty() + + tags.get(plainLong("age")) shouldBe 5L + tags.get(plainLong("nil")) shouldBe null + tags.get(longOption("age")) shouldBe Option(5L) + tags.get(longOption("nil")) shouldBe None + tags.get(longOptional("age")) shouldBe Optional.of(5L) + tags.get(longOptional("nil")) shouldBe Optional.empty() + + tags.get(plainBoolean("isAwesome")) shouldBe true + tags.get(plainBoolean("isUnknown")) shouldBe null + tags.get(booleanOption("isAwesome")) shouldBe Some(true) + tags.get(booleanOption("isUnknown")) shouldBe None + tags.get(booleanOptional("isAwesome")) shouldBe Optional.of(true) + tags.get(booleanOptional("isUnknown")) shouldBe Optional.empty() + + tags.get(coerce("age")) shouldBe "5" + tags.get(coerce("isAwesome")) shouldBe "true" + tags.get(coerce("unknown")) shouldBe "unknown" + } + + "allow iterating over all contained tags" in { + val tags = TagSet.from(Map( + "age" -> 5L, + "name" -> "Kamon", + "isAwesome" -> true, + "hasTracing" -> true, + "website" -> "kamon.io", + "luckyNumber" -> 7L + )) + + tags.iterator().length shouldBe 6 + tags.iterator().find(matchPair("age", 5L)) shouldBe defined + tags.iterator().find(matchPair("luckyNumber", 7L)) shouldBe defined + tags.iterator().find(matchPair("hasTracing", true)) shouldBe defined + tags.iterator().find(matchPair("isAwesome", true)) shouldBe defined + tags.iterator().find(matchPair("website", "kamon.io")) shouldBe defined + tags.iterator().find(matchPair("name", "Kamon")) shouldBe defined + } + + "be equal to other Tags instance with the same tags" in { + TagSet.from(GoodScalaTagMap) shouldBe TagSet.from(GoodScalaTagMap) + TagSet.from(GoodJavaTagMap) shouldBe TagSet.from(GoodJavaTagMap) + } + + "have a readable toString implementation" in { + TagSet.from(GoodScalaTagMap).toString() should include("age=5") + TagSet.from(GoodScalaTagMap).toString() should include("name=Kamon") + TagSet.from(GoodScalaTagMap).toString() should include("isAwesome=true") + } + } + + def matchPair(key: String, value: Any) = { tag: Tag => { + tag match { + case t: Tag.String => t.key == key && t.value == value + case t: Tag.Long => t.key == key && t.value == value + case t: Tag.Boolean => t.key == key && t.value == value + } + + }} + + + val NullString: java.lang.String = null + val NullBoolean: java.lang.Boolean = NullString.asInstanceOf[java.lang.Boolean] + val NullLong: java.lang.Long = null + val EmptyString: java.lang.String = "" + + val GoodScalaTagMap: Map[String, Any] = Map( + "age" -> 5L, + "name" -> "Kamon", + "isAwesome" -> true + ) + + val BadScalaTagMap: Map[String, Any] = Map( + NullString -> NullString, + EmptyString -> NullString, + NullString -> NullString, + EmptyString -> NullString, + EmptyString -> "value", + NullString -> "value", + "key" -> NullString, + "key" -> NullBoolean, + "key" -> NullLong + ) + + val GoodJavaTagMap = GoodScalaTagMap.asJava + val BadJavaTagMap = BadScalaTagMap.asJava + +} diff --git a/kamon-core-tests/src/test/scala/kamon/tag/TagsSpec.scala b/kamon-core-tests/src/test/scala/kamon/tag/TagsSpec.scala deleted file mode 100644 index 1a83e1c9..00000000 --- a/kamon-core-tests/src/test/scala/kamon/tag/TagsSpec.scala +++ /dev/null @@ -1,180 +0,0 @@ -package kamon.tag - -import java.util.Optional - -import org.scalatest.{Matchers, WordSpec} - -import scala.collection.JavaConverters.mapAsJavaMapConverter - -class TagsSpec extends WordSpec with Matchers { - import Lookups._ - - "Tags" should { - "silently drop null and unacceptable keys and/or values when constructed from the companion object builders" in { - Tags.from(NullString, NullString).all().size shouldBe 0 - Tags.from(EmptyString, NullString).all().size shouldBe 0 - Tags.from(EmptyString, "value").all().size shouldBe 0 - Tags.from(NullString, "value").all().size shouldBe 0 - Tags.from("key", NullString).all().size shouldBe 0 - Tags.from("key", NullBoolean).all().size shouldBe 0 - Tags.from("key", NullLong).all().size shouldBe 0 - - Tags.from(BadScalaTagMap).all().size shouldBe 0 - Tags.from(BadJavaTagMap).all().size shouldBe 0 - } - - "silently drop null keys and/or values when created with the .withTag, withTags or .and methods" in { - val tags = Tags.from("initialKey", "initialValue") - .withTag(NullString, NullString) - .withTag(EmptyString, NullString) - .withTag(EmptyString, "value") - .withTag(NullString, "value") - .withTag("key", NullString) - .withTag("key", NullBoolean) - .withTag("key", NullLong) - .and(NullString, NullString) - .and(EmptyString, NullString) - .and(EmptyString, "value") - .and(NullString, "value") - .and("key", NullString) - .and("key", NullBoolean) - .and("key", NullLong) - - tags.all().length shouldBe 1 - tags.all().head.asInstanceOf[Tag.String].key shouldBe "initialKey" - tags.all().head.asInstanceOf[Tag.String].value shouldBe "initialValue" - } - - "create a properly populated instance when valid pairs are provided" in { - Tags.from("isAwesome", true).all().size shouldBe 1 - Tags.from("name", "kamon").all().size shouldBe 1 - Tags.from("age", 5L).all().size shouldBe 1 - - Tags.from(GoodScalaTagMap).all().size shouldBe 3 - Tags.from(GoodJavaTagMap).all().size shouldBe 3 - - Tags.from("initial", "initial") - .withTag("isAwesome", true) - .withTag("name", "Kamon") - .withTag("age", 5L) - .and("isAvailable", true) - .and("website", "kamon.io") - .and("supportedPlatforms", 1L) - .all().size shouldBe 7 - } - - "override pre-existent tags when merging with other Tags instance" in { - val leftTags = Tags.from(GoodScalaTagMap) - val rightTags = Tags - .from("name", "New Kamon") - .and("age", 42L) - .and("isAwesome", false) // just for testing :) - - val tags = leftTags.withTags(rightTags) - tags.get(plain("name")) shouldBe "New Kamon" - tags.get(plainLong("age")) shouldBe 42L - tags.get(plainBoolean("isAwesome")) shouldBe false - - val andTags = tags and leftTags - andTags.get(plain("name")) shouldBe "Kamon" - andTags.get(plainLong("age")) shouldBe 5L - andTags.get(plainBoolean("isAwesome")) shouldBe true - } - - "provide typed access to the contained pairs when looking up values" in { - val tags = Tags.from(GoodScalaTagMap) - - tags.get(plain("name")) shouldBe "Kamon" - tags.get(plain("none")) shouldBe null - tags.get(option("name")) shouldBe Option("Kamon") - tags.get(option("none")) shouldBe None - tags.get(optional("name")) shouldBe Optional.of("Kamon") - tags.get(optional("none")) shouldBe Optional.empty() - - tags.get(plainLong("age")) shouldBe 5L - tags.get(plainLong("nil")) shouldBe null - tags.get(longOption("age")) shouldBe Option(5L) - tags.get(longOption("nil")) shouldBe None - tags.get(longOptional("age")) shouldBe Optional.of(5L) - tags.get(longOptional("nil")) shouldBe Optional.empty() - - tags.get(plainBoolean("isAwesome")) shouldBe true - tags.get(plainBoolean("isUnknown")) shouldBe null - tags.get(booleanOption("isAwesome")) shouldBe Some(true) - tags.get(booleanOption("isUnknown")) shouldBe None - tags.get(booleanOptional("isAwesome")) shouldBe Optional.of(true) - tags.get(booleanOptional("isUnknown")) shouldBe Optional.empty() - - tags.get(coerce("age")) shouldBe "5" - tags.get(coerce("isAwesome")) shouldBe "true" - tags.get(coerce("unknown")) shouldBe "unknown" - } - - "allow iterating over all contained tags" in { - val tags = Tags.from(Map( - "age" -> 5L, - "name" -> "Kamon", - "isAwesome" -> true, - "hasTracing" -> true, - "website" -> "kamon.io", - "luckyNumber" -> 7L - )) - - tags.iterator().length shouldBe 6 - tags.iterator().find(matchPair("age", 5L)) shouldBe defined - tags.iterator().find(matchPair("luckyNumber", 7L)) shouldBe defined - tags.iterator().find(matchPair("hasTracing", true)) shouldBe defined - tags.iterator().find(matchPair("isAwesome", true)) shouldBe defined - tags.iterator().find(matchPair("website", "kamon.io")) shouldBe defined - tags.iterator().find(matchPair("name", "Kamon")) shouldBe defined - } - - "be equal to other Tags instance with the same tags" in { - Tags.from(GoodScalaTagMap) shouldBe Tags.from(GoodScalaTagMap) - Tags.from(GoodJavaTagMap) shouldBe Tags.from(GoodJavaTagMap) - } - - "have a readable toString implementation" in { - Tags.from(GoodScalaTagMap).toString() should include("age=5") - Tags.from(GoodScalaTagMap).toString() should include("name=Kamon") - Tags.from(GoodScalaTagMap).toString() should include("isAwesome=true") - } - } - - def matchPair(key: String, value: Any) = { tag: Tag => { - tag match { - case t: Tag.String => t.key == key && t.value == value - case t: Tag.Long => t.key == key && t.value == value - case t: Tag.Boolean => t.key == key && t.value == value - } - - }} - - - val NullString: java.lang.String = null - val NullBoolean: java.lang.Boolean = NullString.asInstanceOf[java.lang.Boolean] - val NullLong: java.lang.Long = null - val EmptyString: java.lang.String = "" - - val GoodScalaTagMap: Map[String, Any] = Map( - "age" -> 5L, - "name" -> "Kamon", - "isAwesome" -> true - ) - - val BadScalaTagMap: Map[String, Any] = Map( - NullString -> NullString, - EmptyString -> NullString, - NullString -> NullString, - EmptyString -> NullString, - EmptyString -> "value", - NullString -> "value", - "key" -> NullString, - "key" -> NullBoolean, - "key" -> NullLong - ) - - val GoodJavaTagMap = GoodScalaTagMap.asJava - val BadJavaTagMap = BadScalaTagMap.asJava - -} diff --git a/kamon-core/src/main/colfer/Context.colf b/kamon-core/src/main/colfer/Context.colf index f5d9a80b..9bd9ec76 100644 --- a/kamon-core/src/main/colfer/Context.colf +++ b/kamon-core/src/main/colfer/Context.colf @@ -1,11 +1,32 @@ package context type Entry struct { - name text - content binary + key text + value binary +} + +type StringTag struct { + key text + value text +} + +type LongTag struct { + key text + value int64 +} + +type BooleanTag struct { + key text + value bool +} + +type Tags struct { + strings []StringTag + longs []LongTag + booleans []BooleanTag } type Context struct { - tags []text + tags Tags entries []Entry } \ No newline at end of file diff --git a/kamon-core/src/main/java/kamon/context/generated/binary/context/BooleanTag.java b/kamon-core/src/main/java/kamon/context/generated/binary/context/BooleanTag.java new file mode 100644 index 00000000..d8caad09 --- /dev/null +++ b/kamon-core/src/main/java/kamon/context/generated/binary/context/BooleanTag.java @@ -0,0 +1,412 @@ +package kamon.context.generated.binary.context; + + +// Code generated by colf(1); DO NOT EDIT. + + +import static java.lang.String.format; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamException; +import java.io.OutputStream; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.InputMismatchException; +import java.nio.BufferOverflowException; +import java.nio.BufferUnderflowException; + + +/** + * Data bean with built-in serialization support. + + * @author generated by colf(1) + * @see Colfer's home + */ +@javax.annotation.Generated(value="colf(1)", comments="Colfer from schema file Context.colf") +public class BooleanTag implements Serializable { + + /** The upper limit for serial byte sizes. */ + public static int colferSizeMax = 16 * 1024 * 1024; + + + + + public String key; + + public boolean value; + + + /** Default constructor */ + public BooleanTag() { + init(); + } + + + /** Colfer zero values. */ + private void init() { + key = ""; + } + + /** + * {@link #reset(InputStream) Reusable} deserialization of Colfer streams. + */ + public static class Unmarshaller { + + /** The data source. */ + protected InputStream in; + + /** The read buffer. */ + public byte[] buf; + + /** The {@link #buf buffer}'s data start index, inclusive. */ + protected int offset; + + /** The {@link #buf buffer}'s data end index, exclusive. */ + protected int i; + + + /** + * @param in the data source or {@code null}. + * @param buf the initial buffer or {@code null}. + */ + public Unmarshaller(InputStream in, byte[] buf) { + // TODO: better size estimation + if (buf == null || buf.length == 0) + buf = new byte[Math.min(BooleanTag.colferSizeMax, 2048)]; + this.buf = buf; + reset(in); + } + + /** + * Reuses the marshaller. + * @param in the data source or {@code null}. + * @throws IllegalStateException on pending data. + */ + public void reset(InputStream in) { + if (this.i != this.offset) throw new IllegalStateException("colfer: pending data"); + this.in = in; + this.offset = 0; + this.i = 0; + } + + /** + * Deserializes the following object. + * @return the result or {@code null} when EOF. + * @throws IOException from the input stream. + * @throws SecurityException on an upper limit breach defined by {@link #colferSizeMax}. + * @throws InputMismatchException when the data does not match this object's schema. + */ + public BooleanTag next() throws IOException { + if (in == null) return null; + + while (true) { + if (this.i > this.offset) { + try { + BooleanTag o = new BooleanTag(); + this.offset = o.unmarshal(this.buf, this.offset, this.i); + return o; + } catch (BufferUnderflowException e) { + } + } + // not enough data + + if (this.i <= this.offset) { + this.offset = 0; + this.i = 0; + } else if (i == buf.length) { + byte[] src = this.buf; + // TODO: better size estimation + if (offset == 0) this.buf = new byte[Math.min(BooleanTag.colferSizeMax, this.buf.length * 4)]; + System.arraycopy(src, this.offset, this.buf, 0, this.i - this.offset); + this.i -= this.offset; + this.offset = 0; + } + assert this.i < this.buf.length; + + int n = in.read(buf, i, buf.length - i); + if (n < 0) { + if (this.i > this.offset) + throw new InputMismatchException("colfer: pending data with EOF"); + return null; + } + assert n > 0; + i += n; + } + } + + } + + + /** + * Serializes the object. + * @param out the data destination. + * @param buf the initial buffer or {@code null}. + * @return the final buffer. When the serial fits into {@code buf} then the return is {@code buf}. + * Otherwise the return is a new buffer, large enough to hold the whole serial. + * @throws IOException from {@code out}. + * @throws IllegalStateException on an upper limit breach defined by {@link #colferSizeMax}. + */ + public byte[] marshal(OutputStream out, byte[] buf) throws IOException { + // TODO: better size estimation + if (buf == null || buf.length == 0) + buf = new byte[Math.min(BooleanTag.colferSizeMax, 2048)]; + + while (true) { + int i; + try { + i = marshal(buf, 0); + } catch (BufferOverflowException e) { + buf = new byte[Math.min(BooleanTag.colferSizeMax, buf.length * 4)]; + continue; + } + + out.write(buf, 0, i); + return buf; + } + } + + /** + * Serializes the object. + * @param buf the data destination. + * @param offset the initial index for {@code buf}, inclusive. + * @return the final index for {@code buf}, exclusive. + * @throws BufferOverflowException when {@code buf} is too small. + * @throws IllegalStateException on an upper limit breach defined by {@link #colferSizeMax}. + */ + public int marshal(byte[] buf, int offset) { + int i = offset; + + try { + if (! this.key.isEmpty()) { + buf[i++] = (byte) 0; + int start = ++i; + + String s = this.key; + for (int sIndex = 0, sLength = s.length(); sIndex < sLength; sIndex++) { + char c = s.charAt(sIndex); + if (c < '\u0080') { + buf[i++] = (byte) c; + } else if (c < '\u0800') { + buf[i++] = (byte) (192 | c >>> 6); + buf[i++] = (byte) (128 | c & 63); + } else if (c < '\ud800' || c > '\udfff') { + buf[i++] = (byte) (224 | c >>> 12); + buf[i++] = (byte) (128 | c >>> 6 & 63); + buf[i++] = (byte) (128 | c & 63); + } else { + int cp = 0; + if (++sIndex < sLength) cp = Character.toCodePoint(c, s.charAt(sIndex)); + if ((cp >= 1 << 16) && (cp < 1 << 21)) { + buf[i++] = (byte) (240 | cp >>> 18); + buf[i++] = (byte) (128 | cp >>> 12 & 63); + buf[i++] = (byte) (128 | cp >>> 6 & 63); + buf[i++] = (byte) (128 | cp & 63); + } else + buf[i++] = (byte) '?'; + } + } + int size = i - start; + if (size > BooleanTag.colferSizeMax) + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.BooleanTag.key size %d exceeds %d UTF-8 bytes", size, BooleanTag.colferSizeMax)); + + int ii = start - 1; + if (size > 0x7f) { + i++; + for (int x = size; x >= 1 << 14; x >>>= 7) i++; + System.arraycopy(buf, start, buf, i - size, size); + + do { + buf[ii++] = (byte) (size | 0x80); + size >>>= 7; + } while (size > 0x7f); + } + buf[ii] = (byte) size; + } + + if (this.value) { + buf[i++] = (byte) 1; + } + + buf[i++] = (byte) 0x7f; + return i; + } catch (ArrayIndexOutOfBoundsException e) { + if (i - offset > BooleanTag.colferSizeMax) + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.BooleanTag exceeds %d bytes", BooleanTag.colferSizeMax)); + if (i > buf.length) throw new BufferOverflowException(); + throw e; + } + } + + /** + * Deserializes the object. + * @param buf the data source. + * @param offset the initial index for {@code buf}, inclusive. + * @return the final index for {@code buf}, exclusive. + * @throws BufferUnderflowException when {@code buf} is incomplete. (EOF) + * @throws SecurityException on an upper limit breach defined by {@link #colferSizeMax}. + * @throws InputMismatchException when the data does not match this object's schema. + */ + public int unmarshal(byte[] buf, int offset) { + return unmarshal(buf, offset, buf.length); + } + + /** + * Deserializes the object. + * @param buf the data source. + * @param offset the initial index for {@code buf}, inclusive. + * @param end the index limit for {@code buf}, exclusive. + * @return the final index for {@code buf}, exclusive. + * @throws BufferUnderflowException when {@code buf} is incomplete. (EOF) + * @throws SecurityException on an upper limit breach defined by {@link #colferSizeMax}. + * @throws InputMismatchException when the data does not match this object's schema. + */ + public int unmarshal(byte[] buf, int offset, int end) { + if (end > buf.length) end = buf.length; + int i = offset; + + try { + byte header = buf[i++]; + + if (header == (byte) 0) { + int size = 0; + for (int shift = 0; true; shift += 7) { + byte b = buf[i++]; + size |= (b & 0x7f) << shift; + if (shift == 28 || b >= 0) break; + } + if (size < 0 || size > BooleanTag.colferSizeMax) + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.BooleanTag.key size %d exceeds %d UTF-8 bytes", size, BooleanTag.colferSizeMax)); + + int start = i; + i += size; + this.key = new String(buf, start, size, StandardCharsets.UTF_8); + header = buf[i++]; + } + + if (header == (byte) 1) { + this.value = true; + header = buf[i++]; + } + + if (header != (byte) 0x7f) + throw new InputMismatchException(format("colfer: unknown header at byte %d", i - 1)); + } finally { + if (i > end && end - offset < BooleanTag.colferSizeMax) throw new BufferUnderflowException(); + if (i < 0 || i - offset > BooleanTag.colferSizeMax) + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.BooleanTag exceeds %d bytes", BooleanTag.colferSizeMax)); + if (i > end) throw new BufferUnderflowException(); + } + + return i; + } + + // {@link Serializable} version number. + private static final long serialVersionUID = 2L; + + // {@link Serializable} Colfer extension. + private void writeObject(ObjectOutputStream out) throws IOException { + // TODO: better size estimation + byte[] buf = new byte[1024]; + int n; + while (true) try { + n = marshal(buf, 0); + break; + } catch (BufferUnderflowException e) { + buf = new byte[4 * buf.length]; + } + + out.writeInt(n); + out.write(buf, 0, n); + } + + // {@link Serializable} Colfer extension. + private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException { + init(); + + int n = in.readInt(); + byte[] buf = new byte[n]; + in.readFully(buf); + unmarshal(buf, 0); + } + + // {@link Serializable} Colfer extension. + private void readObjectNoData() throws ObjectStreamException { + init(); + } + + /** + * Gets kamon/context/generated/binary/context.BooleanTag.key. + * @return the value. + */ + public String getKey() { + return this.key; + } + + /** + * Sets kamon/context/generated/binary/context.BooleanTag.key. + * @param value the replacement. + */ + public void setKey(String value) { + this.key = value; + } + + /** + * Sets kamon/context/generated/binary/context.BooleanTag.key. + * @param value the replacement. + * @return {link this}. + */ + public BooleanTag withKey(String value) { + this.key = value; + return this; + } + + /** + * Gets kamon/context/generated/binary/context.BooleanTag.value. + * @return the value. + */ + public boolean getValue() { + return this.value; + } + + /** + * Sets kamon/context/generated/binary/context.BooleanTag.value. + * @param value the replacement. + */ + public void setValue(boolean value) { + this.value = value; + } + + /** + * Sets kamon/context/generated/binary/context.BooleanTag.value. + * @param value the replacement. + * @return {link this}. + */ + public BooleanTag withValue(boolean value) { + this.value = value; + return this; + } + + @Override + public final int hashCode() { + int h = 1; + if (this.key != null) h = 31 * h + this.key.hashCode(); + h = 31 * h + (this.value ? 1231 : 1237); + return h; + } + + @Override + public final boolean equals(Object o) { + return o instanceof BooleanTag && equals((BooleanTag) o); + } + + public final boolean equals(BooleanTag o) { + if (o == null) return false; + if (o == this) return true; + return o.getClass() == BooleanTag.class + && (this.key == null ? o.key == null : this.key.equals(o.key)) + && this.value == o.value; + } + +} diff --git a/kamon-core/src/main/java/kamon/context/generated/binary/context/Context.java b/kamon-core/src/main/java/kamon/context/generated/binary/context/Context.java index 4be6d630..3582bfa2 100644 --- a/kamon-core/src/main/java/kamon/context/generated/binary/context/Context.java +++ b/kamon-core/src/main/java/kamon/context/generated/binary/context/Context.java @@ -12,7 +12,6 @@ import java.io.ObjectOutputStream; import java.io.ObjectStreamException; import java.io.OutputStream; import java.io.Serializable; -import java.nio.charset.StandardCharsets; import java.util.InputMismatchException; import java.nio.BufferOverflowException; import java.nio.BufferUnderflowException; @@ -36,7 +35,7 @@ public class Context implements Serializable { - public String[] tags; + public Tags tags; public Entry[] entries; @@ -46,12 +45,10 @@ public class Context implements Serializable { init(); } - private static final String[] _zeroTags = new String[0]; private static final Entry[] _zeroEntries = new Entry[0]; /** Colfer zero values. */ private void init() { - tags = _zeroTags; entries = _zeroEntries; } @@ -147,7 +144,6 @@ public class Context implements Serializable { /** * Serializes the object. - * All {@code null} elements in {@link #tags} will be replaced with {@code ""}. * All {@code null} elements in {@link #entries} will be replaced with a {@code new} value. * @param out the data destination. * @param buf the initial buffer or {@code null}. @@ -177,7 +173,6 @@ public class Context implements Serializable { /** * Serializes the object. - * All {@code null} elements in {@link #tags} will be replaced with {@code ""}. * All {@code null} elements in {@link #entries} will be replaced with a {@code new} value. * @param buf the data destination. * @param offset the initial index for {@code buf}, inclusive. @@ -189,68 +184,9 @@ public class Context implements Serializable { int i = offset; try { - if (this.tags.length != 0) { + if (this.tags != null) { buf[i++] = (byte) 0; - String[] a = this.tags; - - int x = a.length; - if (x > Context.colferListMax) - throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.Context.tags length %d exceeds %d elements", x, Context.colferListMax)); - while (x > 0x7f) { - buf[i++] = (byte) (x | 0x80); - x >>>= 7; - } - buf[i++] = (byte) x; - - for (int ai = 0; ai < a.length; ai++) { - String s = a[ai]; - if (s == null) { - s = ""; - a[ai] = s; - } - - int start = ++i; - - for (int sIndex = 0, sLength = s.length(); sIndex < sLength; sIndex++) { - char c = s.charAt(sIndex); - if (c < '\u0080') { - buf[i++] = (byte) c; - } else if (c < '\u0800') { - buf[i++] = (byte) (192 | c >>> 6); - buf[i++] = (byte) (128 | c & 63); - } else if (c < '\ud800' || c > '\udfff') { - buf[i++] = (byte) (224 | c >>> 12); - buf[i++] = (byte) (128 | c >>> 6 & 63); - buf[i++] = (byte) (128 | c & 63); - } else { - int cp = 0; - if (++sIndex < sLength) cp = Character.toCodePoint(c, s.charAt(sIndex)); - if ((cp >= 1 << 16) && (cp < 1 << 21)) { - buf[i++] = (byte) (240 | cp >>> 18); - buf[i++] = (byte) (128 | cp >>> 12 & 63); - buf[i++] = (byte) (128 | cp >>> 6 & 63); - buf[i++] = (byte) (128 | cp & 63); - } else - buf[i++] = (byte) '?'; - } - } - int size = i - start; - if (size > Context.colferSizeMax) - throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.Context.tags[%d] size %d exceeds %d UTF-8 bytes", ai, size, Context.colferSizeMax)); - - int ii = start - 1; - if (size > 0x7f) { - i++; - for (int y = size; y >= 1 << 14; y >>>= 7) i++; - System.arraycopy(buf, start, buf, i - size, size); - - do { - buf[ii++] = (byte) (size | 0x80); - size >>>= 7; - } while (size > 0x7f); - } - buf[ii] = (byte) size; - } + i = this.tags.marshal(buf, i); } if (this.entries.length != 0) { @@ -317,31 +253,8 @@ public class Context implements Serializable { byte header = buf[i++]; if (header == (byte) 0) { - int length = 0; - for (int shift = 0; true; shift += 7) { - byte b = buf[i++]; - length |= (b & 0x7f) << shift; - if (shift == 28 || b >= 0) break; - } - if (length < 0 || length > Context.colferListMax) - throw new SecurityException(format("colfer: kamon/context/generated/binary/context.Context.tags length %d exceeds %d elements", length, Context.colferListMax)); - - String[] a = new String[length]; - for (int ai = 0; ai < length; ai++) { - int size = 0; - for (int shift = 0; true; shift += 7) { - byte b = buf[i++]; - size |= (b & 0x7f) << shift; - if (shift == 28 || b >= 0) break; - } - if (size < 0 || size > Context.colferSizeMax) - throw new SecurityException(format("colfer: kamon/context/generated/binary/context.Context.tags[%d] size %d exceeds %d UTF-8 bytes", ai, size, Context.colferSizeMax)); - - int start = i; - i += size; - a[ai] = new String(buf, start, size, StandardCharsets.UTF_8); - } - this.tags = a; + this.tags = new Tags(); + i = this.tags.unmarshal(buf, i, end); header = buf[i++]; } @@ -415,7 +328,7 @@ public class Context implements Serializable { * Gets kamon/context/generated/binary/context.Context.tags. * @return the value. */ - public String[] getTags() { + public Tags getTags() { return this.tags; } @@ -423,7 +336,7 @@ public class Context implements Serializable { * Sets kamon/context/generated/binary/context.Context.tags. * @param value the replacement. */ - public void setTags(String[] value) { + public void setTags(Tags value) { this.tags = value; } @@ -432,7 +345,7 @@ public class Context implements Serializable { * @param value the replacement. * @return {link this}. */ - public Context withTags(String[] value) { + public Context withTags(Tags value) { this.tags = value; return this; } @@ -466,7 +379,7 @@ public class Context implements Serializable { @Override public final int hashCode() { int h = 1; - for (String o : this.tags) h = 31 * h + (o == null ? 0 : o.hashCode()); + if (this.tags != null) h = 31 * h + this.tags.hashCode(); for (Entry o : this.entries) h = 31 * h + (o == null ? 0 : o.hashCode()); return h; } @@ -480,7 +393,7 @@ public class Context implements Serializable { if (o == null) return false; if (o == this) return true; return o.getClass() == Context.class - && java.util.Arrays.equals(this.tags, o.tags) + && (this.tags == null ? o.tags == null : this.tags.equals(o.tags)) && java.util.Arrays.equals(this.entries, o.entries); } diff --git a/kamon-core/src/main/java/kamon/context/generated/binary/context/Entry.java b/kamon-core/src/main/java/kamon/context/generated/binary/context/Entry.java index dc75b10d..32213c79 100644 --- a/kamon-core/src/main/java/kamon/context/generated/binary/context/Entry.java +++ b/kamon-core/src/main/java/kamon/context/generated/binary/context/Entry.java @@ -33,9 +33,9 @@ public class Entry implements Serializable { - public String name; + public String key; - public byte[] content; + public byte[] value; /** Default constructor */ @@ -47,8 +47,8 @@ public class Entry implements Serializable { /** Colfer zero values. */ private void init() { - name = ""; - content = _zeroBytes; + key = ""; + value = _zeroBytes; } /** @@ -181,11 +181,11 @@ public class Entry implements Serializable { int i = offset; try { - if (! this.name.isEmpty()) { + if (! this.key.isEmpty()) { buf[i++] = (byte) 0; int start = ++i; - String s = this.name; + String s = this.key; for (int sIndex = 0, sLength = s.length(); sIndex < sLength; sIndex++) { char c = s.charAt(sIndex); if (c < '\u0080') { @@ -211,7 +211,7 @@ public class Entry implements Serializable { } int size = i - start; if (size > Entry.colferSizeMax) - throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.Entry.name size %d exceeds %d UTF-8 bytes", size, Entry.colferSizeMax)); + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.Entry.key size %d exceeds %d UTF-8 bytes", size, Entry.colferSizeMax)); int ii = start - 1; if (size > 0x7f) { @@ -227,12 +227,12 @@ public class Entry implements Serializable { buf[ii] = (byte) size; } - if (this.content.length != 0) { + if (this.value.length != 0) { buf[i++] = (byte) 1; - int size = this.content.length; + int size = this.value.length; if (size > Entry.colferSizeMax) - throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.Entry.content size %d exceeds %d bytes", size, Entry.colferSizeMax)); + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.Entry.value size %d exceeds %d bytes", size, Entry.colferSizeMax)); int x = size; while (x > 0x7f) { @@ -243,7 +243,7 @@ public class Entry implements Serializable { int start = i; i += size; - System.arraycopy(this.content, 0, buf, start, size); + System.arraycopy(this.value, 0, buf, start, size); } buf[i++] = (byte) 0x7f; @@ -294,11 +294,11 @@ public class Entry implements Serializable { if (shift == 28 || b >= 0) break; } if (size < 0 || size > Entry.colferSizeMax) - throw new SecurityException(format("colfer: kamon/context/generated/binary/context.Entry.name size %d exceeds %d UTF-8 bytes", size, Entry.colferSizeMax)); + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.Entry.key size %d exceeds %d UTF-8 bytes", size, Entry.colferSizeMax)); int start = i; i += size; - this.name = new String(buf, start, size, StandardCharsets.UTF_8); + this.key = new String(buf, start, size, StandardCharsets.UTF_8); header = buf[i++]; } @@ -310,12 +310,12 @@ public class Entry implements Serializable { if (shift == 28 || b >= 0) break; } if (size < 0 || size > Entry.colferSizeMax) - throw new SecurityException(format("colfer: kamon/context/generated/binary/context.Entry.content size %d exceeds %d bytes", size, Entry.colferSizeMax)); + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.Entry.value size %d exceeds %d bytes", size, Entry.colferSizeMax)); - this.content = new byte[size]; + this.value = new byte[size]; int start = i; i += size; - System.arraycopy(buf, start, this.content, 0, size); + System.arraycopy(buf, start, this.value, 0, size); header = buf[i++]; } @@ -367,62 +367,62 @@ public class Entry implements Serializable { } /** - * Gets kamon/context/generated/binary/context.Entry.name. + * Gets kamon/context/generated/binary/context.Entry.key. * @return the value. */ - public String getName() { - return this.name; + public String getKey() { + return this.key; } /** - * Sets kamon/context/generated/binary/context.Entry.name. + * Sets kamon/context/generated/binary/context.Entry.key. * @param value the replacement. */ - public void setName(String value) { - this.name = value; + public void setKey(String value) { + this.key = value; } /** - * Sets kamon/context/generated/binary/context.Entry.name. + * Sets kamon/context/generated/binary/context.Entry.key. * @param value the replacement. * @return {link this}. */ - public Entry withName(String value) { - this.name = value; + public Entry withKey(String value) { + this.key = value; return this; } /** - * Gets kamon/context/generated/binary/context.Entry.content. + * Gets kamon/context/generated/binary/context.Entry.value. * @return the value. */ - public byte[] getContent() { - return this.content; + public byte[] getValue() { + return this.value; } /** - * Sets kamon/context/generated/binary/context.Entry.content. + * Sets kamon/context/generated/binary/context.Entry.value. * @param value the replacement. */ - public void setContent(byte[] value) { - this.content = value; + public void setValue(byte[] value) { + this.value = value; } /** - * Sets kamon/context/generated/binary/context.Entry.content. + * Sets kamon/context/generated/binary/context.Entry.value. * @param value the replacement. * @return {link this}. */ - public Entry withContent(byte[] value) { - this.content = value; + public Entry withValue(byte[] value) { + this.value = value; return this; } @Override public final int hashCode() { int h = 1; - if (this.name != null) h = 31 * h + this.name.hashCode(); - for (byte b : this.content) h = 31 * h + b; + if (this.key != null) h = 31 * h + this.key.hashCode(); + for (byte b : this.value) h = 31 * h + b; return h; } @@ -435,8 +435,8 @@ public class Entry implements Serializable { if (o == null) return false; if (o == this) return true; return o.getClass() == Entry.class - && (this.name == null ? o.name == null : this.name.equals(o.name)) - && java.util.Arrays.equals(this.content, o.content); + && (this.key == null ? o.key == null : this.key.equals(o.key)) + && java.util.Arrays.equals(this.value, o.value); } } diff --git a/kamon-core/src/main/java/kamon/context/generated/binary/context/LongTag.java b/kamon-core/src/main/java/kamon/context/generated/binary/context/LongTag.java new file mode 100644 index 00000000..505ba2f0 --- /dev/null +++ b/kamon-core/src/main/java/kamon/context/generated/binary/context/LongTag.java @@ -0,0 +1,443 @@ +package kamon.context.generated.binary.context; + + +// Code generated by colf(1); DO NOT EDIT. + + +import static java.lang.String.format; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamException; +import java.io.OutputStream; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.InputMismatchException; +import java.nio.BufferOverflowException; +import java.nio.BufferUnderflowException; + + +/** + * Data bean with built-in serialization support. + + * @author generated by colf(1) + * @see Colfer's home + */ +@javax.annotation.Generated(value="colf(1)", comments="Colfer from schema file Context.colf") +public class LongTag implements Serializable { + + /** The upper limit for serial byte sizes. */ + public static int colferSizeMax = 16 * 1024 * 1024; + + + + + public String key; + + public long value; + + + /** Default constructor */ + public LongTag() { + init(); + } + + + /** Colfer zero values. */ + private void init() { + key = ""; + } + + /** + * {@link #reset(InputStream) Reusable} deserialization of Colfer streams. + */ + public static class Unmarshaller { + + /** The data source. */ + protected InputStream in; + + /** The read buffer. */ + public byte[] buf; + + /** The {@link #buf buffer}'s data start index, inclusive. */ + protected int offset; + + /** The {@link #buf buffer}'s data end index, exclusive. */ + protected int i; + + + /** + * @param in the data source or {@code null}. + * @param buf the initial buffer or {@code null}. + */ + public Unmarshaller(InputStream in, byte[] buf) { + // TODO: better size estimation + if (buf == null || buf.length == 0) + buf = new byte[Math.min(LongTag.colferSizeMax, 2048)]; + this.buf = buf; + reset(in); + } + + /** + * Reuses the marshaller. + * @param in the data source or {@code null}. + * @throws IllegalStateException on pending data. + */ + public void reset(InputStream in) { + if (this.i != this.offset) throw new IllegalStateException("colfer: pending data"); + this.in = in; + this.offset = 0; + this.i = 0; + } + + /** + * Deserializes the following object. + * @return the result or {@code null} when EOF. + * @throws IOException from the input stream. + * @throws SecurityException on an upper limit breach defined by {@link #colferSizeMax}. + * @throws InputMismatchException when the data does not match this object's schema. + */ + public LongTag next() throws IOException { + if (in == null) return null; + + while (true) { + if (this.i > this.offset) { + try { + LongTag o = new LongTag(); + this.offset = o.unmarshal(this.buf, this.offset, this.i); + return o; + } catch (BufferUnderflowException e) { + } + } + // not enough data + + if (this.i <= this.offset) { + this.offset = 0; + this.i = 0; + } else if (i == buf.length) { + byte[] src = this.buf; + // TODO: better size estimation + if (offset == 0) this.buf = new byte[Math.min(LongTag.colferSizeMax, this.buf.length * 4)]; + System.arraycopy(src, this.offset, this.buf, 0, this.i - this.offset); + this.i -= this.offset; + this.offset = 0; + } + assert this.i < this.buf.length; + + int n = in.read(buf, i, buf.length - i); + if (n < 0) { + if (this.i > this.offset) + throw new InputMismatchException("colfer: pending data with EOF"); + return null; + } + assert n > 0; + i += n; + } + } + + } + + + /** + * Serializes the object. + * @param out the data destination. + * @param buf the initial buffer or {@code null}. + * @return the final buffer. When the serial fits into {@code buf} then the return is {@code buf}. + * Otherwise the return is a new buffer, large enough to hold the whole serial. + * @throws IOException from {@code out}. + * @throws IllegalStateException on an upper limit breach defined by {@link #colferSizeMax}. + */ + public byte[] marshal(OutputStream out, byte[] buf) throws IOException { + // TODO: better size estimation + if (buf == null || buf.length == 0) + buf = new byte[Math.min(LongTag.colferSizeMax, 2048)]; + + while (true) { + int i; + try { + i = marshal(buf, 0); + } catch (BufferOverflowException e) { + buf = new byte[Math.min(LongTag.colferSizeMax, buf.length * 4)]; + continue; + } + + out.write(buf, 0, i); + return buf; + } + } + + /** + * Serializes the object. + * @param buf the data destination. + * @param offset the initial index for {@code buf}, inclusive. + * @return the final index for {@code buf}, exclusive. + * @throws BufferOverflowException when {@code buf} is too small. + * @throws IllegalStateException on an upper limit breach defined by {@link #colferSizeMax}. + */ + public int marshal(byte[] buf, int offset) { + int i = offset; + + try { + if (! this.key.isEmpty()) { + buf[i++] = (byte) 0; + int start = ++i; + + String s = this.key; + for (int sIndex = 0, sLength = s.length(); sIndex < sLength; sIndex++) { + char c = s.charAt(sIndex); + if (c < '\u0080') { + buf[i++] = (byte) c; + } else if (c < '\u0800') { + buf[i++] = (byte) (192 | c >>> 6); + buf[i++] = (byte) (128 | c & 63); + } else if (c < '\ud800' || c > '\udfff') { + buf[i++] = (byte) (224 | c >>> 12); + buf[i++] = (byte) (128 | c >>> 6 & 63); + buf[i++] = (byte) (128 | c & 63); + } else { + int cp = 0; + if (++sIndex < sLength) cp = Character.toCodePoint(c, s.charAt(sIndex)); + if ((cp >= 1 << 16) && (cp < 1 << 21)) { + buf[i++] = (byte) (240 | cp >>> 18); + buf[i++] = (byte) (128 | cp >>> 12 & 63); + buf[i++] = (byte) (128 | cp >>> 6 & 63); + buf[i++] = (byte) (128 | cp & 63); + } else + buf[i++] = (byte) '?'; + } + } + int size = i - start; + if (size > LongTag.colferSizeMax) + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.LongTag.key size %d exceeds %d UTF-8 bytes", size, LongTag.colferSizeMax)); + + int ii = start - 1; + if (size > 0x7f) { + i++; + for (int x = size; x >= 1 << 14; x >>>= 7) i++; + System.arraycopy(buf, start, buf, i - size, size); + + do { + buf[ii++] = (byte) (size | 0x80); + size >>>= 7; + } while (size > 0x7f); + } + buf[ii] = (byte) size; + } + + if (this.value != 0) { + long x = this.value; + if (x < 0) { + x = -x; + buf[i++] = (byte) (1 | 0x80); + } else + buf[i++] = (byte) 1; + for (int n = 0; n < 8 && (x & ~0x7fL) != 0; n++) { + buf[i++] = (byte) (x | 0x80); + x >>>= 7; + } + buf[i++] = (byte) x; + } + + buf[i++] = (byte) 0x7f; + return i; + } catch (ArrayIndexOutOfBoundsException e) { + if (i - offset > LongTag.colferSizeMax) + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.LongTag exceeds %d bytes", LongTag.colferSizeMax)); + if (i > buf.length) throw new BufferOverflowException(); + throw e; + } + } + + /** + * Deserializes the object. + * @param buf the data source. + * @param offset the initial index for {@code buf}, inclusive. + * @return the final index for {@code buf}, exclusive. + * @throws BufferUnderflowException when {@code buf} is incomplete. (EOF) + * @throws SecurityException on an upper limit breach defined by {@link #colferSizeMax}. + * @throws InputMismatchException when the data does not match this object's schema. + */ + public int unmarshal(byte[] buf, int offset) { + return unmarshal(buf, offset, buf.length); + } + + /** + * Deserializes the object. + * @param buf the data source. + * @param offset the initial index for {@code buf}, inclusive. + * @param end the index limit for {@code buf}, exclusive. + * @return the final index for {@code buf}, exclusive. + * @throws BufferUnderflowException when {@code buf} is incomplete. (EOF) + * @throws SecurityException on an upper limit breach defined by {@link #colferSizeMax}. + * @throws InputMismatchException when the data does not match this object's schema. + */ + public int unmarshal(byte[] buf, int offset, int end) { + if (end > buf.length) end = buf.length; + int i = offset; + + try { + byte header = buf[i++]; + + if (header == (byte) 0) { + int size = 0; + for (int shift = 0; true; shift += 7) { + byte b = buf[i++]; + size |= (b & 0x7f) << shift; + if (shift == 28 || b >= 0) break; + } + if (size < 0 || size > LongTag.colferSizeMax) + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.LongTag.key size %d exceeds %d UTF-8 bytes", size, LongTag.colferSizeMax)); + + int start = i; + i += size; + this.key = new String(buf, start, size, StandardCharsets.UTF_8); + header = buf[i++]; + } + + if (header == (byte) 1) { + long x = 0; + for (int shift = 0; true; shift += 7) { + byte b = buf[i++]; + if (shift == 56 || b >= 0) { + x |= (b & 0xffL) << shift; + break; + } + x |= (b & 0x7fL) << shift; + } + this.value = x; + header = buf[i++]; + } else if (header == (byte) (1 | 0x80)) { + long x = 0; + for (int shift = 0; true; shift += 7) { + byte b = buf[i++]; + if (shift == 56 || b >= 0) { + x |= (b & 0xffL) << shift; + break; + } + x |= (b & 0x7fL) << shift; + } + this.value = -x; + header = buf[i++]; + } + + if (header != (byte) 0x7f) + throw new InputMismatchException(format("colfer: unknown header at byte %d", i - 1)); + } finally { + if (i > end && end - offset < LongTag.colferSizeMax) throw new BufferUnderflowException(); + if (i < 0 || i - offset > LongTag.colferSizeMax) + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.LongTag exceeds %d bytes", LongTag.colferSizeMax)); + if (i > end) throw new BufferUnderflowException(); + } + + return i; + } + + // {@link Serializable} version number. + private static final long serialVersionUID = 2L; + + // {@link Serializable} Colfer extension. + private void writeObject(ObjectOutputStream out) throws IOException { + // TODO: better size estimation + byte[] buf = new byte[1024]; + int n; + while (true) try { + n = marshal(buf, 0); + break; + } catch (BufferUnderflowException e) { + buf = new byte[4 * buf.length]; + } + + out.writeInt(n); + out.write(buf, 0, n); + } + + // {@link Serializable} Colfer extension. + private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException { + init(); + + int n = in.readInt(); + byte[] buf = new byte[n]; + in.readFully(buf); + unmarshal(buf, 0); + } + + // {@link Serializable} Colfer extension. + private void readObjectNoData() throws ObjectStreamException { + init(); + } + + /** + * Gets kamon/context/generated/binary/context.LongTag.key. + * @return the value. + */ + public String getKey() { + return this.key; + } + + /** + * Sets kamon/context/generated/binary/context.LongTag.key. + * @param value the replacement. + */ + public void setKey(String value) { + this.key = value; + } + + /** + * Sets kamon/context/generated/binary/context.LongTag.key. + * @param value the replacement. + * @return {link this}. + */ + public LongTag withKey(String value) { + this.key = value; + return this; + } + + /** + * Gets kamon/context/generated/binary/context.LongTag.value. + * @return the value. + */ + public long getValue() { + return this.value; + } + + /** + * Sets kamon/context/generated/binary/context.LongTag.value. + * @param value the replacement. + */ + public void setValue(long value) { + this.value = value; + } + + /** + * Sets kamon/context/generated/binary/context.LongTag.value. + * @param value the replacement. + * @return {link this}. + */ + public LongTag withValue(long value) { + this.value = value; + return this; + } + + @Override + public final int hashCode() { + int h = 1; + if (this.key != null) h = 31 * h + this.key.hashCode(); + h = 31 * h + (int)(this.value ^ this.value >>> 32); + return h; + } + + @Override + public final boolean equals(Object o) { + return o instanceof LongTag && equals((LongTag) o); + } + + public final boolean equals(LongTag o) { + if (o == null) return false; + if (o == this) return true; + return o.getClass() == LongTag.class + && (this.key == null ? o.key == null : this.key.equals(o.key)) + && this.value == o.value; + } + +} diff --git a/kamon-core/src/main/java/kamon/context/generated/binary/context/StringTag.java b/kamon-core/src/main/java/kamon/context/generated/binary/context/StringTag.java new file mode 100644 index 00000000..366744b4 --- /dev/null +++ b/kamon-core/src/main/java/kamon/context/generated/binary/context/StringTag.java @@ -0,0 +1,466 @@ +package kamon.context.generated.binary.context; + + +// Code generated by colf(1); DO NOT EDIT. + + +import static java.lang.String.format; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamException; +import java.io.OutputStream; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.InputMismatchException; +import java.nio.BufferOverflowException; +import java.nio.BufferUnderflowException; + + +/** + * Data bean with built-in serialization support. + + * @author generated by colf(1) + * @see Colfer's home + */ +@javax.annotation.Generated(value="colf(1)", comments="Colfer from schema file Context.colf") +public class StringTag implements Serializable { + + /** The upper limit for serial byte sizes. */ + public static int colferSizeMax = 16 * 1024 * 1024; + + + + + public String key; + + public String value; + + + /** Default constructor */ + public StringTag() { + init(); + } + + + /** Colfer zero values. */ + private void init() { + key = ""; + value = ""; + } + + /** + * {@link #reset(InputStream) Reusable} deserialization of Colfer streams. + */ + public static class Unmarshaller { + + /** The data source. */ + protected InputStream in; + + /** The read buffer. */ + public byte[] buf; + + /** The {@link #buf buffer}'s data start index, inclusive. */ + protected int offset; + + /** The {@link #buf buffer}'s data end index, exclusive. */ + protected int i; + + + /** + * @param in the data source or {@code null}. + * @param buf the initial buffer or {@code null}. + */ + public Unmarshaller(InputStream in, byte[] buf) { + // TODO: better size estimation + if (buf == null || buf.length == 0) + buf = new byte[Math.min(StringTag.colferSizeMax, 2048)]; + this.buf = buf; + reset(in); + } + + /** + * Reuses the marshaller. + * @param in the data source or {@code null}. + * @throws IllegalStateException on pending data. + */ + public void reset(InputStream in) { + if (this.i != this.offset) throw new IllegalStateException("colfer: pending data"); + this.in = in; + this.offset = 0; + this.i = 0; + } + + /** + * Deserializes the following object. + * @return the result or {@code null} when EOF. + * @throws IOException from the input stream. + * @throws SecurityException on an upper limit breach defined by {@link #colferSizeMax}. + * @throws InputMismatchException when the data does not match this object's schema. + */ + public StringTag next() throws IOException { + if (in == null) return null; + + while (true) { + if (this.i > this.offset) { + try { + StringTag o = new StringTag(); + this.offset = o.unmarshal(this.buf, this.offset, this.i); + return o; + } catch (BufferUnderflowException e) { + } + } + // not enough data + + if (this.i <= this.offset) { + this.offset = 0; + this.i = 0; + } else if (i == buf.length) { + byte[] src = this.buf; + // TODO: better size estimation + if (offset == 0) this.buf = new byte[Math.min(StringTag.colferSizeMax, this.buf.length * 4)]; + System.arraycopy(src, this.offset, this.buf, 0, this.i - this.offset); + this.i -= this.offset; + this.offset = 0; + } + assert this.i < this.buf.length; + + int n = in.read(buf, i, buf.length - i); + if (n < 0) { + if (this.i > this.offset) + throw new InputMismatchException("colfer: pending data with EOF"); + return null; + } + assert n > 0; + i += n; + } + } + + } + + + /** + * Serializes the object. + * @param out the data destination. + * @param buf the initial buffer or {@code null}. + * @return the final buffer. When the serial fits into {@code buf} then the return is {@code buf}. + * Otherwise the return is a new buffer, large enough to hold the whole serial. + * @throws IOException from {@code out}. + * @throws IllegalStateException on an upper limit breach defined by {@link #colferSizeMax}. + */ + public byte[] marshal(OutputStream out, byte[] buf) throws IOException { + // TODO: better size estimation + if (buf == null || buf.length == 0) + buf = new byte[Math.min(StringTag.colferSizeMax, 2048)]; + + while (true) { + int i; + try { + i = marshal(buf, 0); + } catch (BufferOverflowException e) { + buf = new byte[Math.min(StringTag.colferSizeMax, buf.length * 4)]; + continue; + } + + out.write(buf, 0, i); + return buf; + } + } + + /** + * Serializes the object. + * @param buf the data destination. + * @param offset the initial index for {@code buf}, inclusive. + * @return the final index for {@code buf}, exclusive. + * @throws BufferOverflowException when {@code buf} is too small. + * @throws IllegalStateException on an upper limit breach defined by {@link #colferSizeMax}. + */ + public int marshal(byte[] buf, int offset) { + int i = offset; + + try { + if (! this.key.isEmpty()) { + buf[i++] = (byte) 0; + int start = ++i; + + String s = this.key; + for (int sIndex = 0, sLength = s.length(); sIndex < sLength; sIndex++) { + char c = s.charAt(sIndex); + if (c < '\u0080') { + buf[i++] = (byte) c; + } else if (c < '\u0800') { + buf[i++] = (byte) (192 | c >>> 6); + buf[i++] = (byte) (128 | c & 63); + } else if (c < '\ud800' || c > '\udfff') { + buf[i++] = (byte) (224 | c >>> 12); + buf[i++] = (byte) (128 | c >>> 6 & 63); + buf[i++] = (byte) (128 | c & 63); + } else { + int cp = 0; + if (++sIndex < sLength) cp = Character.toCodePoint(c, s.charAt(sIndex)); + if ((cp >= 1 << 16) && (cp < 1 << 21)) { + buf[i++] = (byte) (240 | cp >>> 18); + buf[i++] = (byte) (128 | cp >>> 12 & 63); + buf[i++] = (byte) (128 | cp >>> 6 & 63); + buf[i++] = (byte) (128 | cp & 63); + } else + buf[i++] = (byte) '?'; + } + } + int size = i - start; + if (size > StringTag.colferSizeMax) + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.StringTag.key size %d exceeds %d UTF-8 bytes", size, StringTag.colferSizeMax)); + + int ii = start - 1; + if (size > 0x7f) { + i++; + for (int x = size; x >= 1 << 14; x >>>= 7) i++; + System.arraycopy(buf, start, buf, i - size, size); + + do { + buf[ii++] = (byte) (size | 0x80); + size >>>= 7; + } while (size > 0x7f); + } + buf[ii] = (byte) size; + } + + if (! this.value.isEmpty()) { + buf[i++] = (byte) 1; + int start = ++i; + + String s = this.value; + for (int sIndex = 0, sLength = s.length(); sIndex < sLength; sIndex++) { + char c = s.charAt(sIndex); + if (c < '\u0080') { + buf[i++] = (byte) c; + } else if (c < '\u0800') { + buf[i++] = (byte) (192 | c >>> 6); + buf[i++] = (byte) (128 | c & 63); + } else if (c < '\ud800' || c > '\udfff') { + buf[i++] = (byte) (224 | c >>> 12); + buf[i++] = (byte) (128 | c >>> 6 & 63); + buf[i++] = (byte) (128 | c & 63); + } else { + int cp = 0; + if (++sIndex < sLength) cp = Character.toCodePoint(c, s.charAt(sIndex)); + if ((cp >= 1 << 16) && (cp < 1 << 21)) { + buf[i++] = (byte) (240 | cp >>> 18); + buf[i++] = (byte) (128 | cp >>> 12 & 63); + buf[i++] = (byte) (128 | cp >>> 6 & 63); + buf[i++] = (byte) (128 | cp & 63); + } else + buf[i++] = (byte) '?'; + } + } + int size = i - start; + if (size > StringTag.colferSizeMax) + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.StringTag.value size %d exceeds %d UTF-8 bytes", size, StringTag.colferSizeMax)); + + int ii = start - 1; + if (size > 0x7f) { + i++; + for (int x = size; x >= 1 << 14; x >>>= 7) i++; + System.arraycopy(buf, start, buf, i - size, size); + + do { + buf[ii++] = (byte) (size | 0x80); + size >>>= 7; + } while (size > 0x7f); + } + buf[ii] = (byte) size; + } + + buf[i++] = (byte) 0x7f; + return i; + } catch (ArrayIndexOutOfBoundsException e) { + if (i - offset > StringTag.colferSizeMax) + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.StringTag exceeds %d bytes", StringTag.colferSizeMax)); + if (i > buf.length) throw new BufferOverflowException(); + throw e; + } + } + + /** + * Deserializes the object. + * @param buf the data source. + * @param offset the initial index for {@code buf}, inclusive. + * @return the final index for {@code buf}, exclusive. + * @throws BufferUnderflowException when {@code buf} is incomplete. (EOF) + * @throws SecurityException on an upper limit breach defined by {@link #colferSizeMax}. + * @throws InputMismatchException when the data does not match this object's schema. + */ + public int unmarshal(byte[] buf, int offset) { + return unmarshal(buf, offset, buf.length); + } + + /** + * Deserializes the object. + * @param buf the data source. + * @param offset the initial index for {@code buf}, inclusive. + * @param end the index limit for {@code buf}, exclusive. + * @return the final index for {@code buf}, exclusive. + * @throws BufferUnderflowException when {@code buf} is incomplete. (EOF) + * @throws SecurityException on an upper limit breach defined by {@link #colferSizeMax}. + * @throws InputMismatchException when the data does not match this object's schema. + */ + public int unmarshal(byte[] buf, int offset, int end) { + if (end > buf.length) end = buf.length; + int i = offset; + + try { + byte header = buf[i++]; + + if (header == (byte) 0) { + int size = 0; + for (int shift = 0; true; shift += 7) { + byte b = buf[i++]; + size |= (b & 0x7f) << shift; + if (shift == 28 || b >= 0) break; + } + if (size < 0 || size > StringTag.colferSizeMax) + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.StringTag.key size %d exceeds %d UTF-8 bytes", size, StringTag.colferSizeMax)); + + int start = i; + i += size; + this.key = new String(buf, start, size, StandardCharsets.UTF_8); + header = buf[i++]; + } + + if (header == (byte) 1) { + int size = 0; + for (int shift = 0; true; shift += 7) { + byte b = buf[i++]; + size |= (b & 0x7f) << shift; + if (shift == 28 || b >= 0) break; + } + if (size < 0 || size > StringTag.colferSizeMax) + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.StringTag.value size %d exceeds %d UTF-8 bytes", size, StringTag.colferSizeMax)); + + int start = i; + i += size; + this.value = new String(buf, start, size, StandardCharsets.UTF_8); + header = buf[i++]; + } + + if (header != (byte) 0x7f) + throw new InputMismatchException(format("colfer: unknown header at byte %d", i - 1)); + } finally { + if (i > end && end - offset < StringTag.colferSizeMax) throw new BufferUnderflowException(); + if (i < 0 || i - offset > StringTag.colferSizeMax) + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.StringTag exceeds %d bytes", StringTag.colferSizeMax)); + if (i > end) throw new BufferUnderflowException(); + } + + return i; + } + + // {@link Serializable} version number. + private static final long serialVersionUID = 2L; + + // {@link Serializable} Colfer extension. + private void writeObject(ObjectOutputStream out) throws IOException { + // TODO: better size estimation + byte[] buf = new byte[1024]; + int n; + while (true) try { + n = marshal(buf, 0); + break; + } catch (BufferUnderflowException e) { + buf = new byte[4 * buf.length]; + } + + out.writeInt(n); + out.write(buf, 0, n); + } + + // {@link Serializable} Colfer extension. + private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException { + init(); + + int n = in.readInt(); + byte[] buf = new byte[n]; + in.readFully(buf); + unmarshal(buf, 0); + } + + // {@link Serializable} Colfer extension. + private void readObjectNoData() throws ObjectStreamException { + init(); + } + + /** + * Gets kamon/context/generated/binary/context.StringTag.key. + * @return the value. + */ + public String getKey() { + return this.key; + } + + /** + * Sets kamon/context/generated/binary/context.StringTag.key. + * @param value the replacement. + */ + public void setKey(String value) { + this.key = value; + } + + /** + * Sets kamon/context/generated/binary/context.StringTag.key. + * @param value the replacement. + * @return {link this}. + */ + public StringTag withKey(String value) { + this.key = value; + return this; + } + + /** + * Gets kamon/context/generated/binary/context.StringTag.value. + * @return the value. + */ + public String getValue() { + return this.value; + } + + /** + * Sets kamon/context/generated/binary/context.StringTag.value. + * @param value the replacement. + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Sets kamon/context/generated/binary/context.StringTag.value. + * @param value the replacement. + * @return {link this}. + */ + public StringTag withValue(String value) { + this.value = value; + return this; + } + + @Override + public final int hashCode() { + int h = 1; + if (this.key != null) h = 31 * h + this.key.hashCode(); + if (this.value != null) h = 31 * h + this.value.hashCode(); + return h; + } + + @Override + public final boolean equals(Object o) { + return o instanceof StringTag && equals((StringTag) o); + } + + public final boolean equals(StringTag o) { + if (o == null) return false; + if (o == this) return true; + return o.getClass() == StringTag.class + && (this.key == null ? o.key == null : this.key.equals(o.key)) + && (this.value == null ? o.value == null : this.value.equals(o.value)); + } + +} diff --git a/kamon-core/src/main/java/kamon/context/generated/binary/context/Tags.java b/kamon-core/src/main/java/kamon/context/generated/binary/context/Tags.java new file mode 100644 index 00000000..ce4d66db --- /dev/null +++ b/kamon-core/src/main/java/kamon/context/generated/binary/context/Tags.java @@ -0,0 +1,513 @@ +package kamon.context.generated.binary.context; + + +// Code generated by colf(1); DO NOT EDIT. + + +import static java.lang.String.format; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamException; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.InputMismatchException; +import java.nio.BufferOverflowException; +import java.nio.BufferUnderflowException; + + +/** + * Data bean with built-in serialization support. + + * @author generated by colf(1) + * @see Colfer's home + */ +@javax.annotation.Generated(value="colf(1)", comments="Colfer from schema file Context.colf") +public class Tags implements Serializable { + + /** The upper limit for serial byte sizes. */ + public static int colferSizeMax = 16 * 1024 * 1024; + + /** The upper limit for the number of elements in a list. */ + public static int colferListMax = 64 * 1024; + + + + + public StringTag[] strings; + + public LongTag[] longs; + + public BooleanTag[] booleans; + + + /** Default constructor */ + public Tags() { + init(); + } + + private static final StringTag[] _zeroStrings = new StringTag[0]; + private static final LongTag[] _zeroLongs = new LongTag[0]; + private static final BooleanTag[] _zeroBooleans = new BooleanTag[0]; + + /** Colfer zero values. */ + private void init() { + strings = _zeroStrings; + longs = _zeroLongs; + booleans = _zeroBooleans; + } + + /** + * {@link #reset(InputStream) Reusable} deserialization of Colfer streams. + */ + public static class Unmarshaller { + + /** The data source. */ + protected InputStream in; + + /** The read buffer. */ + public byte[] buf; + + /** The {@link #buf buffer}'s data start index, inclusive. */ + protected int offset; + + /** The {@link #buf buffer}'s data end index, exclusive. */ + protected int i; + + + /** + * @param in the data source or {@code null}. + * @param buf the initial buffer or {@code null}. + */ + public Unmarshaller(InputStream in, byte[] buf) { + // TODO: better size estimation + if (buf == null || buf.length == 0) + buf = new byte[Math.min(Tags.colferSizeMax, 2048)]; + this.buf = buf; + reset(in); + } + + /** + * Reuses the marshaller. + * @param in the data source or {@code null}. + * @throws IllegalStateException on pending data. + */ + public void reset(InputStream in) { + if (this.i != this.offset) throw new IllegalStateException("colfer: pending data"); + this.in = in; + this.offset = 0; + this.i = 0; + } + + /** + * Deserializes the following object. + * @return the result or {@code null} when EOF. + * @throws IOException from the input stream. + * @throws SecurityException on an upper limit breach defined by either {@link #colferSizeMax} or {@link #colferListMax}. + * @throws InputMismatchException when the data does not match this object's schema. + */ + public Tags next() throws IOException { + if (in == null) return null; + + while (true) { + if (this.i > this.offset) { + try { + Tags o = new Tags(); + this.offset = o.unmarshal(this.buf, this.offset, this.i); + return o; + } catch (BufferUnderflowException e) { + } + } + // not enough data + + if (this.i <= this.offset) { + this.offset = 0; + this.i = 0; + } else if (i == buf.length) { + byte[] src = this.buf; + // TODO: better size estimation + if (offset == 0) this.buf = new byte[Math.min(Tags.colferSizeMax, this.buf.length * 4)]; + System.arraycopy(src, this.offset, this.buf, 0, this.i - this.offset); + this.i -= this.offset; + this.offset = 0; + } + assert this.i < this.buf.length; + + int n = in.read(buf, i, buf.length - i); + if (n < 0) { + if (this.i > this.offset) + throw new InputMismatchException("colfer: pending data with EOF"); + return null; + } + assert n > 0; + i += n; + } + } + + } + + + /** + * Serializes the object. + * All {@code null} elements in {@link #strings} will be replaced with a {@code new} value. + * All {@code null} elements in {@link #longs} will be replaced with a {@code new} value. + * All {@code null} elements in {@link #booleans} will be replaced with a {@code new} value. + * @param out the data destination. + * @param buf the initial buffer or {@code null}. + * @return the final buffer. When the serial fits into {@code buf} then the return is {@code buf}. + * Otherwise the return is a new buffer, large enough to hold the whole serial. + * @throws IOException from {@code out}. + * @throws IllegalStateException on an upper limit breach defined by either {@link #colferSizeMax} or {@link #colferListMax}. + */ + public byte[] marshal(OutputStream out, byte[] buf) throws IOException { + // TODO: better size estimation + if (buf == null || buf.length == 0) + buf = new byte[Math.min(Tags.colferSizeMax, 2048)]; + + while (true) { + int i; + try { + i = marshal(buf, 0); + } catch (BufferOverflowException e) { + buf = new byte[Math.min(Tags.colferSizeMax, buf.length * 4)]; + continue; + } + + out.write(buf, 0, i); + return buf; + } + } + + /** + * Serializes the object. + * All {@code null} elements in {@link #strings} will be replaced with a {@code new} value. + * All {@code null} elements in {@link #longs} will be replaced with a {@code new} value. + * All {@code null} elements in {@link #booleans} will be replaced with a {@code new} value. + * @param buf the data destination. + * @param offset the initial index for {@code buf}, inclusive. + * @return the final index for {@code buf}, exclusive. + * @throws BufferOverflowException when {@code buf} is too small. + * @throws IllegalStateException on an upper limit breach defined by either {@link #colferSizeMax} or {@link #colferListMax}. + */ + public int marshal(byte[] buf, int offset) { + int i = offset; + + try { + if (this.strings.length != 0) { + buf[i++] = (byte) 0; + StringTag[] a = this.strings; + + int x = a.length; + if (x > Tags.colferListMax) + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.Tags.strings length %d exceeds %d elements", x, Tags.colferListMax)); + while (x > 0x7f) { + buf[i++] = (byte) (x | 0x80); + x >>>= 7; + } + buf[i++] = (byte) x; + + for (int ai = 0; ai < a.length; ai++) { + StringTag o = a[ai]; + if (o == null) { + o = new StringTag(); + a[ai] = o; + } + i = o.marshal(buf, i); + } + } + + if (this.longs.length != 0) { + buf[i++] = (byte) 1; + LongTag[] a = this.longs; + + int x = a.length; + if (x > Tags.colferListMax) + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.Tags.longs length %d exceeds %d elements", x, Tags.colferListMax)); + while (x > 0x7f) { + buf[i++] = (byte) (x | 0x80); + x >>>= 7; + } + buf[i++] = (byte) x; + + for (int ai = 0; ai < a.length; ai++) { + LongTag o = a[ai]; + if (o == null) { + o = new LongTag(); + a[ai] = o; + } + i = o.marshal(buf, i); + } + } + + if (this.booleans.length != 0) { + buf[i++] = (byte) 2; + BooleanTag[] a = this.booleans; + + int x = a.length; + if (x > Tags.colferListMax) + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.Tags.booleans length %d exceeds %d elements", x, Tags.colferListMax)); + while (x > 0x7f) { + buf[i++] = (byte) (x | 0x80); + x >>>= 7; + } + buf[i++] = (byte) x; + + for (int ai = 0; ai < a.length; ai++) { + BooleanTag o = a[ai]; + if (o == null) { + o = new BooleanTag(); + a[ai] = o; + } + i = o.marshal(buf, i); + } + } + + buf[i++] = (byte) 0x7f; + return i; + } catch (ArrayIndexOutOfBoundsException e) { + if (i - offset > Tags.colferSizeMax) + throw new IllegalStateException(format("colfer: kamon/context/generated/binary/context.Tags exceeds %d bytes", Tags.colferSizeMax)); + if (i > buf.length) throw new BufferOverflowException(); + throw e; + } + } + + /** + * Deserializes the object. + * @param buf the data source. + * @param offset the initial index for {@code buf}, inclusive. + * @return the final index for {@code buf}, exclusive. + * @throws BufferUnderflowException when {@code buf} is incomplete. (EOF) + * @throws SecurityException on an upper limit breach defined by either {@link #colferSizeMax} or {@link #colferListMax}. + * @throws InputMismatchException when the data does not match this object's schema. + */ + public int unmarshal(byte[] buf, int offset) { + return unmarshal(buf, offset, buf.length); + } + + /** + * Deserializes the object. + * @param buf the data source. + * @param offset the initial index for {@code buf}, inclusive. + * @param end the index limit for {@code buf}, exclusive. + * @return the final index for {@code buf}, exclusive. + * @throws BufferUnderflowException when {@code buf} is incomplete. (EOF) + * @throws SecurityException on an upper limit breach defined by either {@link #colferSizeMax} or {@link #colferListMax}. + * @throws InputMismatchException when the data does not match this object's schema. + */ + public int unmarshal(byte[] buf, int offset, int end) { + if (end > buf.length) end = buf.length; + int i = offset; + + try { + byte header = buf[i++]; + + if (header == (byte) 0) { + int length = 0; + for (int shift = 0; true; shift += 7) { + byte b = buf[i++]; + length |= (b & 0x7f) << shift; + if (shift == 28 || b >= 0) break; + } + if (length < 0 || length > Tags.colferListMax) + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.Tags.strings length %d exceeds %d elements", length, Tags.colferListMax)); + + StringTag[] a = new StringTag[length]; + for (int ai = 0; ai < length; ai++) { + StringTag o = new StringTag(); + i = o.unmarshal(buf, i, end); + a[ai] = o; + } + this.strings = a; + header = buf[i++]; + } + + if (header == (byte) 1) { + int length = 0; + for (int shift = 0; true; shift += 7) { + byte b = buf[i++]; + length |= (b & 0x7f) << shift; + if (shift == 28 || b >= 0) break; + } + if (length < 0 || length > Tags.colferListMax) + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.Tags.longs length %d exceeds %d elements", length, Tags.colferListMax)); + + LongTag[] a = new LongTag[length]; + for (int ai = 0; ai < length; ai++) { + LongTag o = new LongTag(); + i = o.unmarshal(buf, i, end); + a[ai] = o; + } + this.longs = a; + header = buf[i++]; + } + + if (header == (byte) 2) { + int length = 0; + for (int shift = 0; true; shift += 7) { + byte b = buf[i++]; + length |= (b & 0x7f) << shift; + if (shift == 28 || b >= 0) break; + } + if (length < 0 || length > Tags.colferListMax) + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.Tags.booleans length %d exceeds %d elements", length, Tags.colferListMax)); + + BooleanTag[] a = new BooleanTag[length]; + for (int ai = 0; ai < length; ai++) { + BooleanTag o = new BooleanTag(); + i = o.unmarshal(buf, i, end); + a[ai] = o; + } + this.booleans = a; + header = buf[i++]; + } + + if (header != (byte) 0x7f) + throw new InputMismatchException(format("colfer: unknown header at byte %d", i - 1)); + } finally { + if (i > end && end - offset < Tags.colferSizeMax) throw new BufferUnderflowException(); + if (i < 0 || i - offset > Tags.colferSizeMax) + throw new SecurityException(format("colfer: kamon/context/generated/binary/context.Tags exceeds %d bytes", Tags.colferSizeMax)); + if (i > end) throw new BufferUnderflowException(); + } + + return i; + } + + // {@link Serializable} version number. + private static final long serialVersionUID = 3L; + + // {@link Serializable} Colfer extension. + private void writeObject(ObjectOutputStream out) throws IOException { + // TODO: better size estimation + byte[] buf = new byte[1024]; + int n; + while (true) try { + n = marshal(buf, 0); + break; + } catch (BufferUnderflowException e) { + buf = new byte[4 * buf.length]; + } + + out.writeInt(n); + out.write(buf, 0, n); + } + + // {@link Serializable} Colfer extension. + private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException { + init(); + + int n = in.readInt(); + byte[] buf = new byte[n]; + in.readFully(buf); + unmarshal(buf, 0); + } + + // {@link Serializable} Colfer extension. + private void readObjectNoData() throws ObjectStreamException { + init(); + } + + /** + * Gets kamon/context/generated/binary/context.Tags.strings. + * @return the value. + */ + public StringTag[] getStrings() { + return this.strings; + } + + /** + * Sets kamon/context/generated/binary/context.Tags.strings. + * @param value the replacement. + */ + public void setStrings(StringTag[] value) { + this.strings = value; + } + + /** + * Sets kamon/context/generated/binary/context.Tags.strings. + * @param value the replacement. + * @return {link this}. + */ + public Tags withStrings(StringTag[] value) { + this.strings = value; + return this; + } + + /** + * Gets kamon/context/generated/binary/context.Tags.longs. + * @return the value. + */ + public LongTag[] getLongs() { + return this.longs; + } + + /** + * Sets kamon/context/generated/binary/context.Tags.longs. + * @param value the replacement. + */ + public void setLongs(LongTag[] value) { + this.longs = value; + } + + /** + * Sets kamon/context/generated/binary/context.Tags.longs. + * @param value the replacement. + * @return {link this}. + */ + public Tags withLongs(LongTag[] value) { + this.longs = value; + return this; + } + + /** + * Gets kamon/context/generated/binary/context.Tags.booleans. + * @return the value. + */ + public BooleanTag[] getBooleans() { + return this.booleans; + } + + /** + * Sets kamon/context/generated/binary/context.Tags.booleans. + * @param value the replacement. + */ + public void setBooleans(BooleanTag[] value) { + this.booleans = value; + } + + /** + * Sets kamon/context/generated/binary/context.Tags.booleans. + * @param value the replacement. + * @return {link this}. + */ + public Tags withBooleans(BooleanTag[] value) { + this.booleans = value; + return this; + } + + @Override + public final int hashCode() { + int h = 1; + for (StringTag o : this.strings) h = 31 * h + (o == null ? 0 : o.hashCode()); + for (LongTag o : this.longs) h = 31 * h + (o == null ? 0 : o.hashCode()); + for (BooleanTag o : this.booleans) h = 31 * h + (o == null ? 0 : o.hashCode()); + return h; + } + + @Override + public final boolean equals(Object o) { + return o instanceof Tags && equals((Tags) o); + } + + public final boolean equals(Tags o) { + if (o == null) return false; + if (o == this) return true; + return o.getClass() == Tags.class + && java.util.Arrays.equals(this.strings, o.strings) + && java.util.Arrays.equals(this.longs, o.longs) + && java.util.Arrays.equals(this.booleans, o.booleans); + } + +} diff --git a/kamon-core/src/main/scala/kamon/context/BinaryPropagation.scala b/kamon-core/src/main/scala/kamon/context/BinaryPropagation.scala index 75e65c44..296003d5 100644 --- a/kamon-core/src/main/scala/kamon/context/BinaryPropagation.scala +++ b/kamon-core/src/main/scala/kamon/context/BinaryPropagation.scala @@ -20,7 +20,9 @@ import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream, Output import com.typesafe.config.Config import kamon.context.BinaryPropagation.{ByteStreamReader, ByteStreamWriter} -import kamon.context.generated.binary.context.{Context => ColferContext, Entry => ColferEntry} +import kamon.context.generated.binary.context.{Context => ColferContext, Entry => ColferEntry, Tags => ColferTags} +import kamon.context.generated.binary.context.{StringTag => ColferStringTag, LongTag => ColferLongTag, BooleanTag => ColferBooleanTag} +import kamon.tag.{Tag, TagSet} import org.slf4j.LoggerFactory import scala.reflect.ClassTag @@ -152,7 +154,7 @@ object BinaryPropagation { * configured entry readers and writers. */ class Default(settings: Settings) extends Propagation[ByteStreamReader, ByteStreamWriter] { - private val _log = LoggerFactory.getLogger(classOf[BinaryPropagation.Default]) + private val _logger = LoggerFactory.getLogger(classOf[BinaryPropagation.Default]) private val _streamPool = new ThreadLocal[Default.ReusableByteStreamWriter] { override def initialValue(): Default.ReusableByteStreamWriter = new Default.ReusableByteStreamWriter(128) } @@ -170,39 +172,29 @@ object BinaryPropagation { } contextData.failed.foreach { - case NonFatal(t) => _log.warn("Failed to read Context from ByteStreamReader", t) + case NonFatal(t) => _logger.warn("Failed to read Context from ByteStreamReader", t) } contextData.map { colferContext => // Context tags - var tagSectionsCount = colferContext.tags.length - if (tagSectionsCount > 0 && tagSectionsCount % 2 != 0) { - _log.warn("Malformed Context tags found, tags consistency might be compromised") - tagSectionsCount -= 1 + val tagsBuilder = Map.newBuilder[String, Any] + if(colferContext.tags != null) { + colferContext.tags.strings.foreach(t => tagsBuilder += (t.key -> t.value)) + colferContext.tags.longs.foreach(t => tagsBuilder += (t.key -> t.value)) + colferContext.tags.booleans.foreach(t => tagsBuilder += (t.key -> t.value)) } - - val tags = if (tagSectionsCount > 0) { - val tagsBuilder = Map.newBuilder[String, String] - var tagIndex = 0 - while (tagIndex < tagSectionsCount) { - tagsBuilder += (colferContext.tags(tagIndex) -> colferContext.tags(tagIndex + 1)) - tagIndex += 2 - } - tagsBuilder.result() - - } else Map.empty[String, String] - + val tags = TagSet.from(tagsBuilder.result()) // Only reads the entries for which there is a registered reader colferContext.entries.foldLeft(Context.of(tags)) { case (context, entryData) => - settings.incomingEntries.get(entryData.name).map { entryReader => + settings.incomingEntries.get(entryData.key).map { entryReader => var contextWithEntry = context try { - contextWithEntry = entryReader.read(ByteStreamReader.of(entryData.content), context) + contextWithEntry = entryReader.read(ByteStreamReader.of(entryData.value), context) } catch { - case NonFatal(t) => _log.warn("Failed to read entry [{}]", entryData.name.asInstanceOf[Any], t) + case NonFatal(t) => _logger.warn("Failed to read entry [{}]", entryData.key.asInstanceOf[Any], t) } contextWithEntry @@ -218,17 +210,36 @@ object BinaryPropagation { val output = _streamPool.get() val contextOutgoingBuffer = _contextBufferPool.get() - if (context.tags.nonEmpty) { - val tags = Array.ofDim[String](context.tags.size * 2) - var tagIndex = 0 - context.tags.foreach { - case (key, value) => - tags.update(tagIndex, key) - tags.update(tagIndex + 1, value) - tagIndex += 2 + if(context.tags.nonEmpty()) { + val tagsData = new ColferTags() + val strings = Array.newBuilder[ColferStringTag] + val longs = Array.newBuilder[ColferLongTag] + val booleans = Array.newBuilder[ColferBooleanTag] + + context.tags.iterator().foreach { + case t: Tag.String => + val st = new ColferStringTag() + st.setKey(t.key) + st.setValue(t.value) + strings += st + + case t: Tag.Long => + val lt = new ColferLongTag() + lt.setKey(t.key) + lt.setValue(t.value) + longs += lt + + case t: Tag.Boolean => + val bt = new ColferBooleanTag() + bt.setKey(t.key) + bt.setValue(t.value) + booleans += bt } - contextData.tags = tags + tagsData.setStrings(strings.result()) + tagsData.setLongs(longs.result()) + tagsData.setBooleans(booleans.result()) + contextData.setTags(tagsData) } if (context.entries.nonEmpty) { @@ -239,10 +250,10 @@ object BinaryPropagation { output.reset() entryWriter.write(context, output) - colferEntry.name = entryName - colferEntry.content = output.toByteArray() + colferEntry.key = entryName + colferEntry.value = output.toByteArray() } catch { - case NonFatal(t) => _log.warn("Failed to write entry [{}]", entryName.asInstanceOf[Any], t) + case NonFatal(t) => _logger.warn("Failed to write entry [{}]", entryName.asInstanceOf[Any], t) } colferEntry @@ -255,7 +266,7 @@ object BinaryPropagation { val contextSize = contextData.marshal(contextOutgoingBuffer, 0) writer.write(contextOutgoingBuffer, 0, contextSize) } catch { - case NonFatal(t) => _log.warn("Failed to write Context to ByteStreamWriter", t) + case NonFatal(t) => _logger.warn("Failed to write Context to ByteStreamWriter", t) } } } diff --git a/kamon-core/src/main/scala/kamon/context/Context.scala b/kamon-core/src/main/scala/kamon/context/Context.scala index 2a7a382e..054a7897 100644 --- a/kamon-core/src/main/scala/kamon/context/Context.scala +++ b/kamon-core/src/main/scala/kamon/context/Context.scala @@ -16,32 +16,92 @@ package kamon package context -import java.util.{Map => JavaMap} -import scala.collection.JavaConverters._ - -class Context private (val entries: Map[String, Any], val tags: Map[String, String]) { +import kamon.tag.TagSet + + +/** + * An immutable set of information that is tied to the processing of single operation in a service. A Context instance + * can contain tags and entries. + * + * Context tags are built on top of the TagSet abstraction that ships with Kamon and since Kamon knows exactly what + * types of values can be included in a TagSet it can automatically serialize and deserialize them when going over + * HTTP and/or Binary transports. + * + * Context entries can contain any arbitrary type specified by the user, but require additional configuration and + * implementation of entry readers and writers if you need them to go over HTTP and/or Binary transports. + * + * Context instances are meant to be constructed by using the builder functions on the Context companion object. + * + */ +class Context private (val entries: Map[String, Any], val tags: TagSet) { + /** + * Gets an entry from this Context. If the entry is present it's current value is returned, otherwise the empty value + * from the provided key will be returned. + */ def get[T](key: Context.Key[T]): T = entries.getOrElse(key.name, key.emptyValue).asInstanceOf[T] - def getTag(tagKey: String): Option[String] = - tags.get(tagKey) + /** + * Executes a lookup on the context tags. The actual return type depends on the provided lookup instance. Take a look + * at the built-in lookups available on the Lookups companion object. + */ + def getTag[T](lookup: TagSet.Lookup[T]): T = + tags.get(lookup) + + + /** + * Creates a new Context instance that includes the provided key and value. If the provided key was already + * associated with another value then the previous value will be discarded and overwritten with the provided one. + */ def withKey[T](key: Context.Key[T], value: T): Context = new Context(entries.updated(key.name, value), tags) - def withTag(tagKey: String, tagValue: String): Context = - new Context(entries, tags.updated(tagKey, tagValue)) - def withTags(tags: Map[String, String]): Context = - new Context(entries, this.tags ++ tags) + /** + * Creates a new Context instance that includes the provided tag key and value. If the provided tag key was already + * associated with another value then the previous tag value will be discarded and overwritten with the provided one. + */ + def withTag(key: String, value: String): Context = + new Context(entries, tags.withTag(key, value)) + + + /** + * Creates a new Context instance that includes the provided tag key and value. If the provided tag key was already + * associated with another value then the previous tag value will be discarded and overwritten with the provided one. + */ + def withTag(key: String, value: Long): Context = + new Context(entries, tags.withTag(key, value)) + + + /** + * Creates a new Context instance that includes the provided tag key and value. If the provided tag key was already + * associated with another value then the previous tag value will be discarded and overwritten with the provided one. + */ + def withTag(key: String, value: Boolean): Context = + new Context(entries, tags.withTag(key, value)) + + + /** + * Creates a new Context instance that includes the provided tags. If any of the tags in this instance are associated + * to a key present on the provided tags then the previous values will be discarded and overwritten with the provided + * ones. + */ + def withTags(tags: TagSet): Context = + new Context(entries, this.tags.and(tags)) - def withTags(tags: JavaMap[String, String]): Context = - new Context(entries, this.tags ++ tags.asScala.toMap) + /** + * Returns whether this Context does not have any tags and does not have any entries. + */ def isEmpty(): Boolean = entries.isEmpty && tags.isEmpty + + /** + * Returns whether this Context has any information, either as tags or entries. + */ def nonEmpty(): Boolean = !isEmpty() @@ -49,32 +109,48 @@ class Context private (val entries: Map[String, Any], val tags: Map[String, Stri object Context { - val Empty = new Context(Map.empty, Map.empty) + val Empty = new Context(Map.empty, TagSet.Empty) - def of(tags: JavaMap[String, String]): Context = - new Context(Map.empty, tags.asScala.toMap) - - def of(tags: Map[String, String]): Context = + /** + * Creates a new Context instance with the provided tags and no entries. + */ + def of(tags: TagSet): Context = new Context(Map.empty, tags) + + /** + * Creates a new Context instance with the provided key and no tags. + */ def of[T](key: Context.Key[T], value: T): Context = - new Context(Map(key.name -> value), Map.empty) + new Context(Map(key.name -> value), TagSet.Empty) - def of[T](key: Context.Key[T], value: T, tags: JavaMap[String, String]): Context = - new Context(Map(key.name -> value), tags.asScala.toMap) - def of[T](key: Context.Key[T], value: T, tags: Map[String, String]): Context = + /** + * Creates a new Context instance with a single entry and the provided tags. + */ + def of[T](key: Context.Key[T], value: T, tags: TagSet): Context = new Context(Map(key.name -> value), tags) + + /** + * Creates a new Context instance with two entries and no tags. + */ def of[T, U](keyOne: Context.Key[T], valueOne: T, keyTwo: Context.Key[U], valueTwo: U): Context = - new Context(Map(keyOne.name -> valueOne, keyTwo.name -> valueTwo), Map.empty) + new Context(Map(keyOne.name -> valueOne, keyTwo.name -> valueTwo), TagSet.Empty) - def of[T, U](keyOne: Context.Key[T], valueOne: T, keyTwo: Context.Key[U], valueTwo: U, tags: JavaMap[String, String]): Context = - new Context(Map(keyOne.name -> valueOne, keyTwo.name -> valueTwo), tags.asScala.toMap) - def of[T, U](keyOne: Context.Key[T], valueOne: T, keyTwo: Context.Key[U], valueTwo: U, tags: Map[String, String]): Context = + /** + * Creates a new Context instance with two entries and the provided tags. + */ + def of[T, U](keyOne: Context.Key[T], valueOne: T, keyTwo: Context.Key[U], valueTwo: U, tags: TagSet): Context = new Context(Map(keyOne.name -> valueOne, keyTwo.name -> valueTwo), tags) + + /** + * Creates a new Context.Key instance that can be used to insert and retrieve values from the context entries. + * Context keys must have a unique name since they will be looked up in transports by their name and the context + * entries are internally stored using their key name as index. + */ def key[T](name: String, emptyValue: T): Context.Key[T] = new Context.Key(name, emptyValue) diff --git a/kamon-core/src/main/scala/kamon/context/HttpPropagation.scala b/kamon-core/src/main/scala/kamon/context/HttpPropagation.scala index 6a15e2a6..fbee75cc 100644 --- a/kamon-core/src/main/scala/kamon/context/HttpPropagation.scala +++ b/kamon-core/src/main/scala/kamon/context/HttpPropagation.scala @@ -17,6 +17,7 @@ package kamon package context import com.typesafe.config.Config +import kamon.tag.{Tag, TagSet} import org.slf4j.LoggerFactory import scala.reflect.ClassTag @@ -91,7 +92,7 @@ object HttpPropagation { * 3. Read all context entries using the incoming entries configuration. */ override def read(reader: HeaderReader): Context = { - val tags = Map.newBuilder[String, String] + val tags = Map.newBuilder[String, Any] // Tags encoded together in the context tags header. try { @@ -99,7 +100,7 @@ object HttpPropagation { contextTagsHeader.split(";").foreach(tagData => { val tagPair = tagData.split("=") if (tagPair.length == 2) { - tags += (tagPair(0) -> tagPair(1)) + tags += (tagPair(0) -> parseTagValue(tagPair(1))) } }) } @@ -118,7 +119,7 @@ object HttpPropagation { } // Incoming Entries - settings.incomingEntries.foldLeft(Context.of(tags.result())) { + settings.incomingEntries.foldLeft(Context.of(TagSet.from(tags.result()))) { case (context, (entryName, entryDecoder)) => var result = context try { @@ -145,10 +146,12 @@ object HttpPropagation { } // Write tags with specific mappings or append them to the context tags header. - context.tags.foreach { - case (tagKey, tagValue) => settings.tagsMappings.get(tagKey) match { - case Some(mappedHeader) => writer.write(mappedHeader, tagValue) - case None => appendTag(tagKey, tagValue) + context.tags.iterator().foreach { tag => + val tagKey = tag.key + + settings.tagsMappings.get(tagKey) match { + case Some(mappedHeader) => writer.write(mappedHeader, tagValueWithPrefix(tag)) + case None => appendTag(tagKey, Tag.unwrapValue(tag).toString) } } @@ -167,6 +170,54 @@ object HttpPropagation { } } } + + + private val _longTypePrefix = "l:" + private val _booleanTypePrefix = "b:" + private val _booleanTrue = "true" + private val _booleanFalse = "false" + + /** + * Tries to infer and parse a value into one of the supported tag types: String, Long or Boolean by looking for the + * type indicator prefix on the tag value. If the inference fails it will default to treat the value as a String. + */ + private def parseTagValue(value: String): Any = { + if (value.isEmpty || value.length < 2) // Empty and short values definitely do not have type indicators. + value + else { + if(value.startsWith(_longTypePrefix)) { + // Try to parse the content as a Long value. + val remaining = value.substring(2) + try { + java.lang.Long.parseLong(remaining) + } catch { + case _: Throwable => remaining + } + + } else if(value.startsWith(_booleanTypePrefix)) { + + // Try to parse the content as a Boolean value. + val remaining = value.substring(2) + if(remaining.equals(_booleanTrue)) + true + else if(remaining.equals(_booleanFalse)) + false + else + remaining + + } else value + } + } + + /** + * Returns the actual value to be written in the HTTP transport, with a type prefix if applicable. + */ + private def tagValueWithPrefix(tag: Tag): String = tag match { + case t: Tag.String => t.value + case t: Tag.Boolean => _booleanTypePrefix + t.value.toString + case t: Tag.Long => _longTypePrefix + t.value.toString + } + } case class Settings( diff --git a/kamon-core/src/main/scala/kamon/instrumentation/HttpServer.scala b/kamon-core/src/main/scala/kamon/instrumentation/HttpServer.scala index 659da8aa..68975711 100644 --- a/kamon-core/src/main/scala/kamon/instrumentation/HttpServer.scala +++ b/kamon-core/src/main/scala/kamon/instrumentation/HttpServer.scala @@ -6,7 +6,9 @@ import java.time.Duration import com.typesafe.config.Config import kamon.context.Context import kamon.instrumentation.HttpServer.Settings.TagMode -import kamon.metric.MeasurementUnit.{time, information} +import kamon.metric.MeasurementUnit.{information, time} +import kamon.tag.TagSet +import kamon.tag.Lookups.{any, option} import kamon.trace.{IdentityProvider, Span} import kamon.util.GlobPathFilter import org.slf4j.LoggerFactory @@ -348,7 +350,7 @@ object HttpServer { span.disableMetrics() - for { traceIdTag <- settings.traceIDTag; customTraceID <- context.getTag(traceIdTag) } { + for {traceIdTag <- settings.traceIDTagLookup; customTraceID <- context.getTag(traceIdTag) } { val identifier = Kamon.identityProvider.traceIdGenerator().from(customTraceID) if(identifier != IdentityProvider.NoIdentifier) span.withTraceID(identifier) @@ -361,9 +363,12 @@ object HttpServer { } addRequestTag("http.url", request.url, settings.urlTagMode) - addRequestTag("http.method", request.method, settings.urlTagMode) + addRequestTag("http.method", request.method, settings.methodTagMode) settings.contextTags.foreach { - case (tagName, mode) => context.getTag(tagName).foreach(tagValue => addRequestTag(tagName, tagValue, mode)) + case (tagName, mode) => + val tagValue = context.getTag(any(tagName)) + if(tagValue != null) + addRequestTag(tagName, tagValue.toString, mode) } span.start() @@ -385,7 +390,7 @@ object HttpServer { propagationChannel: String, enableServerMetrics: Boolean, enableTracing: Boolean, - traceIDTag: Option[String], + traceIDTagLookup: Option[TagSet.Lookup[Option[String]]], enableSpanMetrics: Boolean, urlTagMode: TagMode, methodTagMode: TagMode, @@ -424,7 +429,10 @@ object HttpServer { // Tracing settings val enableTracing = config.getBoolean("tracing.enabled") - val traceIdTag = Option(config.getString("tracing.preferred-trace-id-tag")).filterNot(_ == "none") + val traceIdTagLookup = Option(config.getString("tracing.preferred-trace-id-tag")) + .filterNot(_ == "none") + .map(option) + val enableSpanMetrics = config.getBoolean("tracing.span-metrics") val urlTagMode = TagMode.from(config.getString("tracing.tags.url")) val methodTagMode = TagMode.from(config.getString("tracing.tags.method")) @@ -441,12 +449,12 @@ object HttpServer { case (pattern, operationName) => (new GlobPathFilter(pattern), operationName) } - Settings( + Settings ( enablePropagation, propagationChannel, enableServerMetrics, enableTracing, - traceIdTag, + traceIdTagLookup, enableSpanMetrics, urlTagMode, methodTagMode, diff --git a/kamon-core/src/main/scala/kamon/metric/Accumulator.scala b/kamon-core/src/main/scala/kamon/metric/Accumulator.scala index bf412980..945feeeb 100644 --- a/kamon-core/src/main/scala/kamon/metric/Accumulator.scala +++ b/kamon-core/src/main/scala/kamon/metric/Accumulator.scala @@ -17,7 +17,7 @@ package kamon.metric import java.time.{Duration, Instant} -import kamon.{Kamon, Tags} +import kamon.{Kamon, STags} import kamon.metric.PeriodSnapshotAccumulator.{MetricDistributionKey, MetricValueKey} import kamon.util.Clock @@ -169,6 +169,6 @@ class PeriodSnapshotAccumulator(duration: Duration, margin: Duration) { } object PeriodSnapshotAccumulator { - case class MetricValueKey(name: String, tags: Tags, unit: MeasurementUnit) - case class MetricDistributionKey(name: String, tags: Tags, unit: MeasurementUnit, dynamicRange: DynamicRange) + case class MetricValueKey(name: String, tags: STags, unit: MeasurementUnit) + case class MetricDistributionKey(name: String, tags: STags, unit: MeasurementUnit, dynamicRange: DynamicRange) } diff --git a/kamon-core/src/main/scala/kamon/metric/Metric.scala b/kamon-core/src/main/scala/kamon/metric/Metric.scala index f5ce7b45..69ef88bc 100644 --- a/kamon-core/src/main/scala/kamon/metric/Metric.scala +++ b/kamon-core/src/main/scala/kamon/metric/Metric.scala @@ -34,12 +34,12 @@ trait Metric[T] { def unit: MeasurementUnit def refine(tags: JTags): T - def refine(tags: Tags): T + def refine(tags: STags): T def refine(tags: (String, String)*): T def refine(tag: String, value: String): T def remove(tags: JTags): Boolean - def remove(tags: Tags): Boolean + def remove(tags: STags): Boolean def remove(tags: (String, String)*): Boolean def remove(tag: String, value: String): Boolean } @@ -52,7 +52,7 @@ trait CounterMetric extends Metric[Counter] with Counter private[kamon] abstract sealed class BaseMetric[T, S](val instrumentType: InstrumentType) extends Metric[T] { - private[kamon] val instruments = TrieMap.empty[Tags, T] + private[kamon] val instruments = TrieMap.empty[STags, T] protected lazy val baseInstrument: T = instruments.atomicGetOrElseUpdate(Map.empty, createInstrument(Map.empty)) override def refine(tags: JTags):T = @@ -72,7 +72,7 @@ private[kamon] abstract sealed class BaseMetric[T, S](val instrumentType: Instru override def remove(tags: JTags):Boolean = remove(tags.asScala.toMap) - override def remove(tags: Tags): Boolean = + override def remove(tags: STags): Boolean = if(tags.nonEmpty) instruments.remove(tags).nonEmpty else false override def remove(tags: (String, String)*): Boolean = @@ -88,7 +88,7 @@ private[kamon] abstract sealed class BaseMetric[T, S](val instrumentType: Instru private[kamon] def incarnations(): Seq[Map[String, String]] = instruments.keys.toSeq - protected def createInstrument(tags: Tags): T + protected def createInstrument(tags: STags): T protected def createSnapshot(instrument: T): S } @@ -106,7 +106,7 @@ private[kamon] final class HistogramMetricImpl(val name: String, val unit: Measu override def record(value: Long, times: Long): Unit = baseInstrument.record(value, times) - override protected def createInstrument(tags: Tags): Histogram = + override protected def createInstrument(tags: STags): Histogram = factory.get().buildHistogram(customDynamicRange)(name, tags, unit) override protected def createSnapshot(instrument: Histogram): MetricDistribution = @@ -118,7 +118,7 @@ private[kamon] final class RangeSamplerMetricImpl(val name: String, val unit: Me extends BaseMetric[RangeSampler, MetricDistribution](RangeSampler) with RangeSamplerMetric { private val logger = LoggerFactory.getLogger(classOf[RangeSamplerMetric]) - private val scheduledSamplers = TrieMap.empty[Tags, ScheduledFuture[_]] + private val scheduledSamplers = TrieMap.empty[STags, ScheduledFuture[_]] def dynamicRange: DynamicRange = baseInstrument.dynamicRange @@ -141,7 +141,7 @@ private[kamon] final class RangeSamplerMetricImpl(val name: String, val unit: Me override def sample(): Unit = baseInstrument.sample() - override protected def createInstrument(tags: Tags): RangeSampler = { + override protected def createInstrument(tags: STags): RangeSampler = { val rangeSampler = factory.get().buildRangeSampler(customDynamicRange, customSampleInterval)(name, tags, unit) val sampleInterval = rangeSampler.sampleInterval.toMillis val scheduledFuture = scheduler.scheduleAtFixedRate(scheduledSampler(rangeSampler), sampleInterval, sampleInterval, TimeUnit.MILLISECONDS) @@ -153,7 +153,7 @@ private[kamon] final class RangeSamplerMetricImpl(val name: String, val unit: Me override def remove(tags: JTags): Boolean = removeAndStopSampler(tags.asScala.toMap) - override def remove(tags: Tags): Boolean = + override def remove(tags: STags): Boolean = removeAndStopSampler(tags) override def remove(tags: (String, String)*): Boolean = @@ -162,7 +162,7 @@ private[kamon] final class RangeSamplerMetricImpl(val name: String, val unit: Me override def remove(tag: String, value: String): Boolean = removeAndStopSampler(Map(tag -> value)) - private def removeAndStopSampler(tags: Tags): Boolean = { + private def removeAndStopSampler(tags: STags): Boolean = { val removed = super.remove(tags) if(removed) scheduledSamplers.remove(tags).foreach(sf => { @@ -190,7 +190,7 @@ private[kamon] final class CounterMetricImpl(val name: String, val unit: Measure override def increment(times: Long): Unit = baseInstrument.increment(times) - override protected def createInstrument(tags: Tags): Counter = + override protected def createInstrument(tags: STags): Counter = factory.get().buildCounter(name, tags, unit) override protected def createSnapshot(instrument: Counter): MetricValue = @@ -215,7 +215,7 @@ private[kamon] final class GaugeMetricImpl(val name: String, val unit: Measureme override def set(value: Long): Unit = baseInstrument.set(value) - override protected def createInstrument(tags: Tags): Gauge = + override protected def createInstrument(tags: STags): Gauge = factory.get().buildGauge(name, tags, unit) override protected def createSnapshot(instrument: Gauge): MetricValue = diff --git a/kamon-core/src/main/scala/kamon/metric/PeriodSnapshot.scala b/kamon-core/src/main/scala/kamon/metric/PeriodSnapshot.scala index 50a5f778..09a0e029 100644 --- a/kamon-core/src/main/scala/kamon/metric/PeriodSnapshot.scala +++ b/kamon-core/src/main/scala/kamon/metric/PeriodSnapshot.scala @@ -39,13 +39,13 @@ case class MetricsSnapshot( * Snapshot for instruments that internally track a single value. Meant to be used for counters and gauges. * */ -case class MetricValue(name: String, tags: Tags, unit: MeasurementUnit, value: Long) +case class MetricValue(name: String, tags: STags, unit: MeasurementUnit, value: Long) /** * Snapshot for instruments that internally the distribution of values in a defined dynamic range. Meant to be used * with histograms and min max counters. */ -case class MetricDistribution(name: String, tags: Tags, unit: MeasurementUnit, dynamicRange: DynamicRange, distribution: Distribution) +case class MetricDistribution(name: String, tags: STags, unit: MeasurementUnit, dynamicRange: DynamicRange, distribution: Distribution) trait Distribution { diff --git a/kamon-core/src/main/scala/kamon/metric/Timer.scala b/kamon-core/src/main/scala/kamon/metric/Timer.scala index 74d203a9..749ac876 100644 --- a/kamon-core/src/main/scala/kamon/metric/Timer.scala +++ b/kamon-core/src/main/scala/kamon/metric/Timer.scala @@ -15,7 +15,7 @@ package kamon.metric -import kamon.{JTags, Tags} +import kamon.{JTags, STags} trait Timer extends Histogram { def start(): StartedTimer @@ -82,7 +82,7 @@ private[kamon] final class TimerMetricImpl(val underlyingHistogram: HistogramMet override def refine(tags: JTags): Timer = refine(tags.asScala.toMap) - override def refine(tags: Tags): Timer = + override def refine(tags: STags): Timer = new TimerImpl(underlyingHistogram.refine(tags)) override def refine(tags: (String, String)*): Timer = @@ -94,7 +94,7 @@ private[kamon] final class TimerMetricImpl(val underlyingHistogram: HistogramMet override def remove(tags: JTags): Boolean = remove(tags.asScala.toMap) - override def remove(tags: Tags): Boolean = + override def remove(tags: STags): Boolean = underlyingHistogram.remove(tags) override def remove(tags: (String, String)*): Boolean = diff --git a/kamon-core/src/main/scala/kamon/package.scala b/kamon-core/src/main/scala/kamon/package.scala index d694206c..3da676cd 100644 --- a/kamon-core/src/main/scala/kamon/package.scala +++ b/kamon-core/src/main/scala/kamon/package.scala @@ -23,7 +23,7 @@ import scala.collection.concurrent.TrieMap package object kamon { - type Tags = Map[String, String] + type STags = Map[String, String] type JTags = java.util.Map[String, String] diff --git a/kamon-core/src/main/scala/kamon/tag/Lookups.scala b/kamon-core/src/main/scala/kamon/tag/Lookups.scala index 44ffb6f4..beb1996a 100644 --- a/kamon-core/src/main/scala/kamon/tag/Lookups.scala +++ b/kamon-core/src/main/scala/kamon/tag/Lookups.scala @@ -3,18 +3,27 @@ package kamon.tag import java.util.Optional import java.lang.{Boolean => JBoolean, Long => JLong, String => JString} -import kamon.tag.Tags.Lookup +import kamon.tag.TagSet.Lookup import scala.reflect.ClassTag object Lookups { + /** + * Finds a value associated to the provided key and returns it. If the key is not present then a null is returned. + */ + def any(key: JString) = new Lookup[Any] { + override def execute(storage: Map[JString, Any]): Any = + findAndTransform(key, storage, _any, null) + } + + /** * Finds a String value associated to the provided key and returns it. If the key is not present or the value * associated with they is not a String then a null is returned. */ def plain(key: JString) = new Lookup[JString] { - override def run(storage: Map[JString, Any]): JString = + override def execute(storage: Map[JString, Any]): JString = findAndTransform(key, storage, _plainString, null) } @@ -24,7 +33,7 @@ object Lookups { * not present or the value associated with they is not a String then a None is returned. */ def option(key: JString) = new Lookup[Option[JString]] { - override def run(storage: Map[JString, Any]): Option[JString] = + override def execute(storage: Map[JString, Any]): Option[JString] = findAndTransform(key, storage, _stringOption, None) } @@ -34,7 +43,7 @@ object Lookups { * is not present or the value associated with they is not a String then Optional.empty() is returned. */ def optional(key: JString) = new Lookup[Optional[String]] { - override def run(storage: Map[String, Any]): Optional[String] = + override def execute(storage: Map[String, Any]): Optional[String] = findAndTransform(key, storage, _stringOptional, Optional.empty()) } @@ -47,7 +56,7 @@ object Lookups { * This lookup type is guaranteed to return a non-null String representation of value. */ def coerce(key: String) = new Lookup[String] { - override def run(storage: Map[String, Any]): String = { + override def execute(storage: Map[String, Any]): String = { val value = storage(key) if(value == null) "unknown" @@ -62,7 +71,7 @@ object Lookups { * associated with they is not a Boolean then a null is returned. */ def plainBoolean(key: String) = new Lookup[JBoolean] { - override def run(storage: Map[String, Any]): JBoolean = + override def execute(storage: Map[String, Any]): JBoolean = findAndTransform(key, storage, _plainBoolean, null) } @@ -72,7 +81,7 @@ object Lookups { * is not present or the value associated with they is not a Boolean then a None is returned. */ def booleanOption(key: String) = new Lookup[Option[JBoolean]] { - override def run(storage: Map[String, Any]): Option[JBoolean] = + override def execute(storage: Map[String, Any]): Option[JBoolean] = findAndTransform(key, storage, _booleanOption, None) } @@ -82,7 +91,7 @@ object Lookups { * is not present or the value associated with they is not a Boolean then Optional.empty() is returned. */ def booleanOptional(key: String) = new Lookup[Optional[JBoolean]] { - override def run(storage: Map[String, Any]): Optional[JBoolean] = + override def execute(storage: Map[String, Any]): Optional[JBoolean] = findAndTransform(key, storage, _booleanOptional, Optional.empty()) } @@ -92,7 +101,7 @@ object Lookups { * associated with they is not a Long then a null is returned. */ def plainLong(key: String) = new Lookup[JLong] { - override def run(storage: Map[String, Any]): JLong = + override def execute(storage: Map[String, Any]): JLong = findAndTransform(key, storage, _plainLong, null) } @@ -102,7 +111,7 @@ object Lookups { * not present or the value associated with they is not a Long then a None is returned. */ def longOption(key: String) = new Lookup[Option[JLong]] { - override def run(storage: Map[String, Any]): Option[JLong] = + override def execute(storage: Map[String, Any]): Option[JLong] = findAndTransform(key, storage, _longOption, None) } @@ -112,7 +121,7 @@ object Lookups { * is not present or the value associated with they is not a Long then Optional.empty() is returned. */ def longOptional(key: String) = new Lookup[Optional[JLong]] { - override def run(storage: Map[String, Any]): Optional[JLong] = + override def execute(storage: Map[String, Any]): Optional[JLong] = findAndTransform(key, storage, _longOptional, Optional.empty()) } @@ -134,6 +143,7 @@ object Lookups { transform(value.asInstanceOf[R]) } + private val _any = (a: Any) => a private val _plainString = (a: JString) => a private val _stringOption = (a: JString) => Option(a) private val _stringOptional = (a: JString) => Optional.of(a) diff --git a/kamon-core/src/main/scala/kamon/tag/Tag.scala b/kamon-core/src/main/scala/kamon/tag/Tag.scala new file mode 100644 index 00000000..69a5d7e7 --- /dev/null +++ b/kamon-core/src/main/scala/kamon/tag/Tag.scala @@ -0,0 +1,50 @@ +package kamon.tag + +import java.lang.{Boolean => JBoolean, Long => JLong, String => JString} + +/** + * Marker trait for allowed Tag implementations. Users are not meant to create implementations of this trait outside + * of Kamon. Furthermore, users of TagSet might never need to interact with these classes but rather perform lookups + * using the lookup DSL. + */ +sealed trait Tag { + def key: JString +} + +object Tag { + + /** + * Represents a String key pointing to a String value. + */ + trait String extends Tag { + def value: JString + } + + + /** + * Represents a String key pointing to a Boolean value. + */ + trait Boolean extends Tag { + def value: JBoolean + } + + + /** + * Represents a String key pointing to a Long value. + */ + trait Long extends Tag { + def value: JLong + } + + + /** + * Returns the value held inside of a Tag instance. This utility function is specially useful when iterating over + * tags but not caring about the concrete tag type. + */ + def unwrapValue(tag: Tag): Any = tag match { + case t: Tag.String => t.value + case t: Tag.Boolean => t.value + case t: Tag.Long => t.value + } +} + diff --git a/kamon-core/src/main/scala/kamon/tag/TagSet.scala b/kamon-core/src/main/scala/kamon/tag/TagSet.scala new file mode 100644 index 00000000..1090ca5a --- /dev/null +++ b/kamon-core/src/main/scala/kamon/tag/TagSet.scala @@ -0,0 +1,328 @@ +package kamon.tag + +import kamon.tag.TagSet.Lookup + +import scala.collection.JavaConverters.asScalaIteratorConverter +import java.lang.{Boolean => JBoolean, Long => JLong, String => JString} + +import org.slf4j.LoggerFactory + +/** + * A immutable collection of key/value pairs with specialized support for storing String keys pointing to String, Long + * and/or Boolean values. + * + * Instances of Tags store all pairs in the same data structure, but preserving type information for the stored pairs + * and providing a simple DSL for accessing those values and expressing type expectations. It is also possible to + * lookup pairs without prescribing a mechanism for handling missing values. I.e. users of this class can decide + * whether to receive a null, java.util.Optional, scala.Option or any other value when looking up a pair. + * + * TagSet instances can only be created from the builder functions on the TagSet companion object. There are two + * different options to read the contained pairs from a Tags instance: + * + * 1. Using the lookup DSL. You can use the Lookup DSL when you know exactly that you are trying to get out of the + * tags instance. The lookup DSL is biased towards String keys since they are by far the most common case. For + * example, to get a given tag as an Option[String] and another as an Option[Boolean] the following code should + * suffice: + * + * import kamon.tag.Tags.Lookup._ + * val tags = Tags.from(tagMap) + * val name = tags.get(option("name")) + * val isSignedIn = tags.get(booleanOption("isSignedIn")) + * + * 2. Using the .all() and .iterator variants. This option requires you to test the returned instances to verify + * whether they are a Tag.String, Tag.Long or Tag.Boolean instance and act accordingly. Fortunately this + * cumbersome operation is rarely necessary on user-facing code. + * + */ +class TagSet private(private val _tags: Map[String, Any]) { + import TagSet.withPair + + /** + * Creates a new TagSet instance that includes the provided key/value pair. If the provided key was already associated + * with another value then the previous value will be discarded and overwritten with the provided one. + */ + def withTag(key: String, value: JString): TagSet = + withPair(this, key, value) + + + /** + * Creates a new TagSet instance that includes the provided key/value pair. If the provided key was already associated + * with another value then the previous value will be discarded and overwritten with the provided one. + */ + def withTag(key: String, value: JBoolean): TagSet = + withPair(this, key, value) + + + /** + * Creates a new TagSet instance that includes the provided key/value pair. If the provided key was already associated + * with another value then the previous value will be discarded and overwritten with the provided one. + */ + def withTag(key: String, value: JLong): TagSet = + withPair(this, key, value) + + + /** + * Creates a new TagSet instance that includes all the tags from the provided Tags instance. If any of the tags in this + * instance are associated to a key present on the provided instance then the previous value will be discarded and + * overwritten with the provided one. + */ + def withTags(other: TagSet): TagSet = + new TagSet(_tags ++ other._tags) + + + /** + * Creates a new TagSet instance that includes the provided key/value pair. If the provided key was already associated + * with another value then the previous value will be discarded and overwritten with the provided one. + */ + def and(key: String, value: JString): TagSet = + withPair(this, key, value) + + + /** + * Creates a new TagSet instance that includes the provided key/value pair. If the provided key was already associated + * with another value then the previous value will be discarded and overwritten with the provided one. + */ + def and(key: String, value: JBoolean): TagSet = + withPair(this, key, value) + + + /** + * Creates a new TagSet instance that includes the provided key/value pair. If the provided key was already associated + * with another value then the previous value will be discarded and overwritten with the provided one. + */ + def and(key: String, value: JLong): TagSet = + withPair(this, key, value) + + + /** + * Creates a new TagSet instance that includes all the tags from the provided Tags instance. If any of the tags in this + * instance are associated to a key present on the provided instance then the previous value will be discarded and + * overwritten with the provided one. + */ + def and(other: TagSet): TagSet = + new TagSet(_tags ++ other._tags) + + + /** + * Returns whether this TagSet instance does not contain any tags. + */ + def isEmpty(): Boolean = + _tags.isEmpty + + + /** + * Returns whether this TagSet instance contains any tags. + */ + def nonEmpty(): Boolean = + _tags.nonEmpty + + + /** + * Executes a tag lookup. The return type of this function will depend on the provided Lookup. Take a look at the + * built-in lookups on the [[Lookups]] companion object for more information. + */ + def get[T](lookup: Lookup[T]): T = + lookup.execute(_tags) + + + /** + * Returns a immutable sequence of tags created from the contained tags internal representation. Calling this method + * will cause the creation of a new data structure. Unless you really need to have all the tags as immutable + * instances it is recommended to use the .iterator() function instead. + * + * The returned sequence contains immutable values and is safe to share across threads. + */ + def all(): Seq[Tag] = + _tags.foldLeft(List.empty[Tag]) { + case (ts, (key, value)) => value match { + case v: String => new immutable.String(key, v) :: ts + case v: Boolean => new immutable.Boolean(key, v) :: ts + case v: Long => new immutable.Long(key, v) :: ts + } + } + + + /** + * Returns an iterator of tags. The underlying iterator reuses the Tag instances to avoid unnecessary intermediate + * allocations and thus, it is not safe to share across threads. The most common case for tags iterators is on + * reporters which will need to iterate through all existent tags only to copy their values into a separate data + * structure that will be sent to the external systems. + */ + def iterator(): Iterator[Tag] = new Iterator[Tag] { + private val _entriesIterator = _tags.iterator + private var _longTag: mutable.Long = null + private var _stringTag: mutable.String = null + private var _booleanTag: mutable.Boolean = null + + override def hasNext: Boolean = + _entriesIterator.hasNext + + override def next(): Tag = { + val (key, value) = _entriesIterator.next() + value match { + case v: String => stringTag(key, v) + case v: Boolean => booleanTag(key, v) + case v: Long => longTag(key, v) + } + } + + private def stringTag(key: JString, value: JString): Tag.String = + if(_stringTag == null) { + _stringTag = new mutable.String(key, value) + _stringTag + } else _stringTag.updated(key, value) + + private def booleanTag(key: JString, value: JBoolean): Tag.Boolean = + if(_booleanTag == null) { + _booleanTag = new mutable.Boolean(key, value) + _booleanTag + } else _booleanTag.updated(key, value) + + private def longTag(key: JString, value: JLong): Tag.Long = + if(_longTag == null) { + _longTag = new mutable.Long(key, value) + _longTag + } else _longTag.updated(key, value) + } + + + override def equals(other: Any): Boolean = + other != null && other.isInstanceOf[TagSet] && other.asInstanceOf[TagSet]._tags == this._tags + + + override def toString: JString = { + val sb = new StringBuilder() + sb.append("Tags{") + + var hasTags = false + _tags.foreach { case (k, v) => + if(hasTags) + sb.append(",") + + sb.append(k) + .append("=") + .append(v) + + hasTags = true + } + + sb.append("}").toString() + } + + private object immutable { + case class String(key: JString, value: JString) extends Tag.String + case class Boolean(key: JString, value: JBoolean) extends Tag.Boolean + case class Long(key: JString, value: JLong) extends Tag.Long + } + + private object mutable { + case class String(var key: JString, var value: JString) extends Tag.String with Updateable[JString] + case class Boolean(var key: JString, var value: JBoolean) extends Tag.Boolean with Updateable[JBoolean] + case class Long(var key: JString, var value: JLong) extends Tag.Long with Updateable[JLong] + + trait Updateable[T] { + var key: JString + var value: T + + def updated(key: JString, value: T): this.type = { + this.key = key + this.value = value + this + } + } + } +} + +object TagSet { + + /** + * A valid instance of tags that doesn't contain any pairs. + */ + val Empty = new TagSet(Map.empty.withDefaultValue(null)) + + + /** + * Construct a new TagSet instance with a single key/value pair. + */ + def from(key: String, value: JString): TagSet = + withPair(Empty, key, value) + + + /** + * Construct a new TagSet instance with a single key/value pair. + */ + def from(key: String, value: JBoolean): TagSet = + withPair(Empty, key, value) + + + /** + * Construct a new TagSet instance with a single key/value pair. + */ + def from(key: String, value: JLong): TagSet = + withPair(Empty, key, value) + + + /** + * Constructs a new TagSet instance from a Map. The returned TagSet will only contain the entries that have String, + * Long or Boolean values from the supplied map, any other entry in the map will be ignored. + */ + def from(map: Map[String, Any]): TagSet = + new TagSet(map.filter { case (k, v) => isValidPair(k, v) } withDefaultValue(null)) + + + /** + * Constructs a new TagSet instance from a Map. The returned TagSet will only contain the entries that have String, + * Long or Boolean values from the supplied map, any other entry in the map will be ignored. + */ + def from(map: java.util.Map[String, Any]): TagSet = { + val allowedTags = Map.newBuilder[String, Any] + map.entrySet() + .iterator() + .asScala + .foreach(e => if(isValidPair(e.getKey, e.getValue)) allowedTags += (e.getKey -> e.getValue)) + + new TagSet(allowedTags.result().withDefaultValue(null)) + } + + + private val _logger = LoggerFactory.getLogger(classOf[TagSet]) + + private def withPair(parent: TagSet, key: String, value: Any): TagSet = + if(isValidPair(key, value)) + new TagSet(parent._tags.updated(key, value)) + else + parent + + private def isValidPair(key: String, value: Any): Boolean = { + val isValidKey = key != null && key.nonEmpty + val isValidValue = isAllowedTagValue(value) + val isValid = isValidKey && isValidValue + + if(!isValid && _logger.isDebugEnabled) { + if(!isValidKey && !isValidValue) + _logger.debug(s"Dismissing tag with invalid key [$key] and invalid value [$value]") + else if(!isValidKey) + _logger.debug(s"Dismissing tag with invalid key [$key] and value [$value]") + else + _logger.debug(s"Dismissing tag with key [$key] and invalid value [$value]") + } + + isValid + } + + private def isAllowedTagValue(v: Any): Boolean = + v != null && (v.isInstanceOf[String] || v.isInstanceOf[Boolean] || v.isInstanceOf[Long]) + + + /** + * Describes a strategy to lookup values from a TagSet instance. Implementations of this interface will be provided + * with the actual data structure containing the tags and must perform any necessary runtime type checks to ensure + * that the returned value is in assignable to the expected type T. + * + * Several implementation are provided in the Lookup companion object and it is recommended to import and use those + * definitions when looking up keys from a Tags instance. + */ + trait Lookup[T] { + def execute(storage: Map[String, Any]): T + } +} \ No newline at end of file diff --git a/kamon-core/src/main/scala/kamon/tag/Tags.scala b/kamon-core/src/main/scala/kamon/tag/Tags.scala deleted file mode 100644 index b7813da6..00000000 --- a/kamon-core/src/main/scala/kamon/tag/Tags.scala +++ /dev/null @@ -1,346 +0,0 @@ -package kamon.tag - -import kamon.tag.Tags.Lookup - -import scala.collection.JavaConverters.asScalaIteratorConverter -import java.lang.{Boolean => JBoolean, Long => JLong, String => JString} - -import org.slf4j.LoggerFactory - - -/** - * Marker trait for allowed Tag implementations. Users are not meant to create implementations of this trait outside - * of Kamon. - */ -sealed trait Tag - -object Tag { - - /** - * Represents a String key pointing to a String value. - */ - trait String extends Tag { - def key: JString - def value: JString - } - - /** - * Represents a String key pointing to a Boolean value. - */ - trait Boolean extends Tag { - def key: JString - def value: JBoolean - } - - /** - * Represents a String key pointing to a Long value. - */ - trait Long extends Tag { - def key: JString - def value: JLong - } -} - - -/** - * A immutable collection of key/value pairs with specialized support for storing String keys pointing to String, Long - * and/or Boolean values. - * - * Instances of Tags store all pairs in the same data structure, but preserving type information for the stored pairs - * and providing a simple DSL for accessing those values and expressing type expectations. It is also possible to - * lookup pairs without prescribing a mechanism for handling missing values. I.e. users of this class can decide - * whether to receive a null, java.util.Optional, scala.Option or any other value when looking up a pair. - * - * Tags can only be created from the builder functions on the Tags companion object. There are two different options - * to read the contained pairs from a Tags instance: - * - * 1. Using the lookup DSL. You can use the Lookup DSL when you know exactly that you are trying to get out of the - * tags instance. The lookup DSL is biased towards String keys since they are by far the most common case. For - * example, to get a given tag as an Option[String] and another as an Option[Boolean] the following code should - * suffice: - * - * import kamon.tag.Tags.Lookup._ - * val tags = Tags.from(tagMap) - * val name = tags.get(option("name")) - * val isSignedIn = tags.get(booleanOption("isSignedIn")) - * - * 2. Using the .all() and .iterator variants. This option requires you to test the returned instances to verify - * whether they are a Tag.String, Tag.Long or Tag.Boolean instance and act accordingly. Fortunately this - * cumbersome operation is rarely necessary on user-facing code. - * - */ -class Tags private(private val _tags: Map[String, Any]) { - import Tags.withPair - - /** - * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated - * with another value then the previous value will be discarded and overwritten with the provided one. - */ - def withTag(key: String, value: JString): Tags = - withPair(this, key, value) - - - /** - * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated - * with another value then the previous value will be discarded and overwritten with the provided one. - */ - def withTag(key: String, value: JBoolean): Tags = - withPair(this, key, value) - - - /** - * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated - * with another value then the previous value will be discarded and overwritten with the provided one. - */ - def withTag(key: String, value: JLong): Tags = - withPair(this, key, value) - - - /** - * Creates a new Tags instance that includes all the tags from the provided Tags instance. If any of the tags in this - * instance are associated to a key present on the provided instance then the previous value will be discarded and - * overwritten with the provided one. - */ - def withTags(other: Tags): Tags = - new Tags(_tags ++ other._tags) - - - /** - * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated - * with another value then the previous value will be discarded and overwritten with the provided one. - */ - def and(key: String, value: JString): Tags = - withPair(this, key, value) - - - /** - * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated - * with another value then the previous value will be discarded and overwritten with the provided one. - */ - def and(key: String, value: JBoolean): Tags = - withPair(this, key, value) - - - /** - * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated - * with another value then the previous value will be discarded and overwritten with the provided one. - */ - def and(key: String, value: JLong): Tags = - withPair(this, key, value) - - - /** - * Creates a new Tags instance that includes all the tags from the provided Tags instance. If any of the tags in this - * instance are associated to a key present on the provided instance then the previous value will be discarded and - * overwritten with the provided one. - */ - def and(other: Tags): Tags = - new Tags(_tags ++ other._tags) - - /** - * Executes a tag lookup on the instance. The return type of this function will depend on the provided Lookup - * instance. Take a look at the built-in lookups on the Tags.Lookup companion object for more information. - */ - def get[T](lookup: Lookup[T]): T = - lookup.run(_tags) - - /** - * Returns a immutable sequence of tags created from the contained tags internal representation. Calling this method - * will cause the creation of a new data structure. Unless you really need to have all the tags as immutable - * instances it is recommended to use the .iterator() function instead. - * - * The returned sequence contains immutable values and is safe to share across threads. - */ - def all(): Seq[Tag] = - _tags.foldLeft(List.empty[Tag]) { - case (ts, (key, value)) => value match { - case v: String => new immutable.String(key, v) :: ts - case v: Boolean => new immutable.Boolean(key, v) :: ts - case v: Long => new immutable.Long(key, v) :: ts - } - } - - /** - * Returns an iterator of tags. The underlying iterator reuses the Tag instances to avoid unnecessary intermediate - * allocations and thus, it is not safe to share across threads. The most common case for tags iterators is on - * reporters which will need to iterate through all existent tags only to copy their values into a separate data - * structure that will be sent to the external systems. - */ - def iterator(): Iterator[Tag] = new Iterator[Tag] { - private val _entriesIterator = _tags.iterator - private var _longTag: mutable.Long = null - private var _stringTag: mutable.String = null - private var _booleanTag: mutable.Boolean = null - - override def hasNext: Boolean = - _entriesIterator.hasNext - - override def next(): Tag = { - val (key, value) = _entriesIterator.next() - value match { - case v: String => stringTag(key, v) - case v: Boolean => booleanTag(key, v) - case v: Long => longTag(key, v) - } - } - - private def stringTag(key: JString, value: JString): Tag.String = - if(_stringTag == null) { - _stringTag = new mutable.String(key, value) - _stringTag - } else _stringTag.updated(key, value) - - private def booleanTag(key: JString, value: JBoolean): Tag.Boolean = - if(_booleanTag == null) { - _booleanTag = new mutable.Boolean(key, value) - _booleanTag - } else _booleanTag.updated(key, value) - - private def longTag(key: JString, value: JLong): Tag.Long = - if(_longTag == null) { - _longTag = new mutable.Long(key, value) - _longTag - } else _longTag.updated(key, value) - } - - - override def equals(other: Any): Boolean = - other != null && other.isInstanceOf[Tags] && other.asInstanceOf[Tags]._tags == this._tags - - - override def toString: JString = { - val sb = new StringBuilder() - sb.append("Tags{") - - var hasTags = false - _tags.foreach { case (k, v) => - if(hasTags) - sb.append(",") - - sb.append(k) - .append("=") - .append(v) - - hasTags = true - } - - sb.append("}").toString() - } - - private object immutable { - class String(val key: JString, val value: JString) extends Tag.String - class Boolean(val key: JString, val value: JBoolean) extends Tag.Boolean - class Long(val key: JString, val value: JLong) extends Tag.Long - } - - private object mutable { - class String(var key: JString, var value: JString) extends Tag.String with Updateable[JString] - class Boolean(var key: JString, var value: JBoolean) extends Tag.Boolean with Updateable[JBoolean] - class Long(var key: JString, var value: JLong) extends Tag.Long with Updateable[JLong] - - trait Updateable[T] { - var key: JString - var value: T - - def updated(key: JString, value: T): this.type = { - this.key = key - this.value = value - this - } - } - } -} - -object Tags { - - /** - * A valid instance of tags that doesn't contain any pairs. - */ - val Empty = new Tags(Map.empty.withDefaultValue(null)) - - - /** - * Construct a new Tags instance with a single key/value pair. - */ - def from(key: String, value: JString): Tags = - withPair(Empty, key, value) - - - /** - * Construct a new Tags instance with a single key/value pair. - */ - def from(key: String, value: JBoolean): Tags = - withPair(Empty, key, value) - - - /** - * Construct a new Tags instance with a single key/value pair. - */ - def from(key: String, value: JLong): Tags = - withPair(Empty, key, value) - - - /** - * Constructs a new Tags instance from a Map. The returned Tags will only contain the entries that have String, Long - * or Boolean values from the supplied map, any other entry in the map will be ignored. - */ - def from(map: Map[String, Any]): Tags = - new Tags(map.filter { case (k, v) => isValidPair(k, v) } withDefaultValue(null)) - - - /** - * Constructs a new Tags instance from a Map. The returned Tags will only contain the entries that have String, Long - * or Boolean values from the supplied map, any other entry in the map will be ignored. - */ - def from(map: java.util.Map[String, Any]): Tags = { - val allowedTags = Map.newBuilder[String, Any] - map.entrySet() - .iterator() - .asScala - .foreach(e => if(isValidPair(e.getKey, e.getValue)) allowedTags += (e.getKey -> e.getValue)) - - new Tags(allowedTags.result().withDefaultValue(null)) - } - - - private val _logger = LoggerFactory.getLogger(classOf[Tags]) - - private def withPair(parent: Tags, key: String, value: Any): Tags = - if(isValidPair(key, value)) - new Tags(parent._tags.updated(key, value)) - else - parent - - private def isValidPair(key: String, value: Any): Boolean = { - val isValidKey = key != null && key.nonEmpty - val isValidValue = isAllowedTagValue(value) - val isValid = isValidKey && isValidValue - - if(!isValid && _logger.isDebugEnabled) { - if(!isValidKey && !isValidValue) - _logger.debug(s"Dismissing tag with invalid key [$key] and invalid value [$value]") - else if(!isValidKey) - _logger.debug(s"Dismissing tag with invalid key [$key] and value [$value]") - else - _logger.debug(s"Dismissing tag with key [$key] and invalid value [$value]") - } - - isValid - } - - private def isAllowedTagValue(v: Any): Boolean = - v != null && (v.isInstanceOf[String] || v.isInstanceOf[Boolean] || v.isInstanceOf[Long]) - - - /** - * Describes a strategy to lookup values from a Tags instance. Implementations of this interface will be provided - * with the actual data structure containing the tags and must perform any necessary runtime type checks to ensure - * that the returned value is in assignable to the expected type T. - * - * Several implementation are provided in the Lookup companion object and it is recommended to import and use those - * definitions when looking up keys from a Tags instance. - */ - trait Lookup[T] { - def run(storage: Map[String, Any]): T - } -} \ No newline at end of file -- cgit v1.2.3 From 2392fb02c3259d7f0b41ff410641accd818bc5d4 Mon Sep 17 00:00:00 2001 From: Ivan Topolnjak Date: Mon, 25 Mar 2019 16:29:21 +0100 Subject: Introduce a TagSet builder and use UnifiedMap from Eclipse Collections as the underlying storage for TagSets. --- build.sbt | 12 +- .../kamon/bench/TagSetCreationBenchmark.scala | 42 ++++ .../scala/kamon/bench/TagSetLookupBenchmark.scala | 55 +++++ kamon-core/src/main/scala/kamon/tag/Lookups.scala | 30 +-- kamon-core/src/main/scala/kamon/tag/TagSet.scala | 245 +++++++++++++++------ 5 files changed, 291 insertions(+), 93 deletions(-) create mode 100644 kamon-core-bench/src/main/scala/kamon/bench/TagSetCreationBenchmark.scala create mode 100644 kamon-core-bench/src/main/scala/kamon/bench/TagSetLookupBenchmark.scala diff --git a/build.sbt b/build.sbt index 5f62b887..538b3804 100644 --- a/build.sbt +++ b/build.sbt @@ -47,6 +47,7 @@ val commonSettings = Seq( ShadeRule.rename("com.grack.nanojson.**" -> "kamon.lib.@0").inAll, ShadeRule.rename("org.jctools.**" -> "kamon.lib.@0").inAll, ShadeRule.rename("fi.iki.elonen.**" -> "kamon.lib.@0").inAll, + ShadeRule.rename("org.eclipse.**" -> "kamon.lib.@0").inAll, ), assemblyExcludedJars in assembly := { val cp = (fullClasspath in assembly).value @@ -56,7 +57,7 @@ val commonSettings = Seq( packageBin in Compile := assembly.value, assemblyJarName in assembly := s"${moduleName.value}_${scalaBinaryVersion.value}-${version.value}.jar", pomPostProcess := { originalPom => { - val shadedGroups = Seq("org.hdrhistogram", "org.jctools", "org.nanohttpd", "com.grack") + val shadedGroups = Seq("org.hdrhistogram", "org.jctools", "org.nanohttpd", "com.grack", "org.eclipse.collections") val filterShadedDependencies = new RuleTransformer(new RewriteRule { override def transform(n: Node): Seq[Node] = { if(n.label == "dependency") { @@ -79,10 +80,11 @@ lazy val core = (project in file("kamon-core")) buildInfoKeys := Seq[BuildInfoKey](version), buildInfoPackage := "kamon.status", libraryDependencies ++= Seq( - "com.typesafe" % "config" % "1.3.1", - "org.hdrhistogram" % "HdrHistogram" % "2.1.9", - "org.jctools" % "jctools-core" % "2.1.1", - "org.slf4j" % "slf4j-api" % "1.7.25" + "com.typesafe" % "config" % "1.3.1", + "org.hdrhistogram" % "HdrHistogram" % "2.1.9", + "org.jctools" % "jctools-core" % "2.1.1", + "org.eclipse.collections" % "eclipse-collections" % "9.2.0", + "org.slf4j" % "slf4j-api" % "1.7.25" ) ) diff --git a/kamon-core-bench/src/main/scala/kamon/bench/TagSetCreationBenchmark.scala b/kamon-core-bench/src/main/scala/kamon/bench/TagSetCreationBenchmark.scala new file mode 100644 index 00000000..9b8f1d7a --- /dev/null +++ b/kamon-core-bench/src/main/scala/kamon/bench/TagSetCreationBenchmark.scala @@ -0,0 +1,42 @@ +package kamon.bench + +import java.util.concurrent.TimeUnit + +import kamon.tag.TagSet +import org.openjdk.jmh.annotations._ + +@BenchmarkMode(Array(Mode.AverageTime)) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(1) +@State(Scope.Benchmark) +class TagSetCreationBenchmark { + + @Param(Array("1", "2", "3", "4", "5", "6")) + var tagCount: Int = 1 + + @Benchmark + def createTagSetFromIndividualKeys(): TagSet = { + var tags = TagSet.Empty + tags = tags.withTag("http.method", "POST") + if(tagCount > 1) tags = tags.withTag("http.url", "http://localhost:8080/test") + if(tagCount > 2) tags = tags.withTag("http.status_code", 200L) + if(tagCount > 3) tags = tags.withTag("error", false) + if(tagCount > 4) tags = tags.withTag("userID", "abcdef") + if(tagCount > 5) tags = tags.withTag("correlationID", "0123456") + + tags + } + + @Benchmark + def createTagSetFromBuilder(): TagSet = { + val tags = TagSet.builder() + tags.add("http.method", "POST") + if(tagCount > 1) tags.add("http.url", "http://localhost:8080/test") + if(tagCount > 2) tags.add("http.status_code", 200L) + if(tagCount > 3) tags.add("error", false) + if(tagCount > 4) tags.add("userID", "abcdef") + if(tagCount > 5) tags.add("correlationID", "0123456") + + tags.create() + } +} diff --git a/kamon-core-bench/src/main/scala/kamon/bench/TagSetLookupBenchmark.scala b/kamon-core-bench/src/main/scala/kamon/bench/TagSetLookupBenchmark.scala new file mode 100644 index 00000000..b8a63d84 --- /dev/null +++ b/kamon-core-bench/src/main/scala/kamon/bench/TagSetLookupBenchmark.scala @@ -0,0 +1,55 @@ +package kamon.bench + +import java.util.concurrent.TimeUnit + +import kamon.tag.TagSet +import org.openjdk.jmh.annotations._ +import kamon.tag.Lookups.any + +@BenchmarkMode(Array(Mode.AverageTime)) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(1) +@State(Scope.Benchmark) +class TagSetLookupBenchmark { + + def builderTags() = TagSet.builder() + .add("http.url", "http://localhost:8080/test") + .add("http.status_code", 200L) + .add("error", false) + .add("userID", "abcdef") + .add("correlationID", "0123456") + .create() + + def keyByKeyTags() = TagSet.Empty + .withTag("http.url", "http://localhost:8080/test") + .withTag("http.status_code", 200L) + .withTag("error", false) + .withTag("userID", "abcdef") + .withTag("correlationID", "0123456") + + + val builderLeft = builderTags() + val builderRight = builderTags() + val keyByKeyLeft = keyByKeyTags() + val keyByKeyRight = keyByKeyTags() + + @Benchmark + def equalityOnBuilderTagSets(): Boolean = { + builderLeft == builderRight + } + + @Benchmark + def equalityOnKeyByKeyTagSets(): Boolean = { + keyByKeyLeft == keyByKeyRight + } + + @Benchmark + def anyLookupOnBuilderTagSet(): Any = { + builderLeft.get(any("userID")) + } + + @Benchmark + def anyLookupOnKeyByKeyTagSet(): Any = { + keyByKeyLeft.get(any("userID")) + } +} diff --git a/kamon-core/src/main/scala/kamon/tag/Lookups.scala b/kamon-core/src/main/scala/kamon/tag/Lookups.scala index beb1996a..9390f472 100644 --- a/kamon-core/src/main/scala/kamon/tag/Lookups.scala +++ b/kamon-core/src/main/scala/kamon/tag/Lookups.scala @@ -13,7 +13,7 @@ object Lookups { * Finds a value associated to the provided key and returns it. If the key is not present then a null is returned. */ def any(key: JString) = new Lookup[Any] { - override def execute(storage: Map[JString, Any]): Any = + override def execute(storage: TagSet.Storage): Any = findAndTransform(key, storage, _any, null) } @@ -23,7 +23,7 @@ object Lookups { * associated with they is not a String then a null is returned. */ def plain(key: JString) = new Lookup[JString] { - override def execute(storage: Map[JString, Any]): JString = + override def execute(storage: TagSet.Storage): JString = findAndTransform(key, storage, _plainString, null) } @@ -33,7 +33,7 @@ object Lookups { * not present or the value associated with they is not a String then a None is returned. */ def option(key: JString) = new Lookup[Option[JString]] { - override def execute(storage: Map[JString, Any]): Option[JString] = + override def execute(storage: TagSet.Storage): Option[JString] = findAndTransform(key, storage, _stringOption, None) } @@ -43,7 +43,7 @@ object Lookups { * is not present or the value associated with they is not a String then Optional.empty() is returned. */ def optional(key: JString) = new Lookup[Optional[String]] { - override def execute(storage: Map[String, Any]): Optional[String] = + override def execute(storage: TagSet.Storage): Optional[String] = findAndTransform(key, storage, _stringOptional, Optional.empty()) } @@ -56,8 +56,8 @@ object Lookups { * This lookup type is guaranteed to return a non-null String representation of value. */ def coerce(key: String) = new Lookup[String] { - override def execute(storage: Map[String, Any]): String = { - val value = storage(key) + override def execute(storage: TagSet.Storage): String = { + val value = storage.get(key) if(value == null) "unknown" else @@ -71,7 +71,7 @@ object Lookups { * associated with they is not a Boolean then a null is returned. */ def plainBoolean(key: String) = new Lookup[JBoolean] { - override def execute(storage: Map[String, Any]): JBoolean = + override def execute(storage: TagSet.Storage): JBoolean = findAndTransform(key, storage, _plainBoolean, null) } @@ -81,7 +81,7 @@ object Lookups { * is not present or the value associated with they is not a Boolean then a None is returned. */ def booleanOption(key: String) = new Lookup[Option[JBoolean]] { - override def execute(storage: Map[String, Any]): Option[JBoolean] = + override def execute(storage: TagSet.Storage): Option[JBoolean] = findAndTransform(key, storage, _booleanOption, None) } @@ -91,7 +91,7 @@ object Lookups { * is not present or the value associated with they is not a Boolean then Optional.empty() is returned. */ def booleanOptional(key: String) = new Lookup[Optional[JBoolean]] { - override def execute(storage: Map[String, Any]): Optional[JBoolean] = + override def execute(storage: TagSet.Storage): Optional[JBoolean] = findAndTransform(key, storage, _booleanOptional, Optional.empty()) } @@ -101,7 +101,7 @@ object Lookups { * associated with they is not a Long then a null is returned. */ def plainLong(key: String) = new Lookup[JLong] { - override def execute(storage: Map[String, Any]): JLong = + override def execute(storage: TagSet.Storage): JLong = findAndTransform(key, storage, _plainLong, null) } @@ -111,7 +111,7 @@ object Lookups { * not present or the value associated with they is not a Long then a None is returned. */ def longOption(key: String) = new Lookup[Option[JLong]] { - override def execute(storage: Map[String, Any]): Option[JLong] = + override def execute(storage: TagSet.Storage): Option[JLong] = findAndTransform(key, storage, _longOption, None) } @@ -121,7 +121,7 @@ object Lookups { * is not present or the value associated with they is not a Long then Optional.empty() is returned. */ def longOptional(key: String) = new Lookup[Optional[JLong]] { - override def execute(storage: Map[String, Any]): Optional[JLong] = + override def execute(storage: TagSet.Storage): Optional[JLong] = findAndTransform(key, storage, _longOptional, Optional.empty()) } @@ -129,13 +129,13 @@ object Lookups { //////////////////////////////////////////////////////////////// // Transformation helpers for the lookup DSL // //////////////////////////////////////////////////////////////// - - private def findAndTransform[T, R](key: String, storage: Map[String, Any], transform: R => T, default: T) + @inline + private def findAndTransform[T, R](key: String, storage: TagSet.Storage, transform: R => T, default: T) (implicit ct: ClassTag[R]): T = { // This assumes that this code will only be used to lookup values from a Tags instance // for which the underlying map always has "null" as the default value. - val value = storage(key) + val value = storage.get(key) if(value == null || !ct.runtimeClass.isInstance(value)) default diff --git a/kamon-core/src/main/scala/kamon/tag/TagSet.scala b/kamon-core/src/main/scala/kamon/tag/TagSet.scala index 1090ca5a..c304a9df 100644 --- a/kamon-core/src/main/scala/kamon/tag/TagSet.scala +++ b/kamon-core/src/main/scala/kamon/tag/TagSet.scala @@ -2,9 +2,10 @@ package kamon.tag import kamon.tag.TagSet.Lookup -import scala.collection.JavaConverters.asScalaIteratorConverter import java.lang.{Boolean => JBoolean, Long => JLong, String => JString} +import java.util.function.BiConsumer +import org.eclipse.collections.impl.map.mutable.UnifiedMap import org.slf4j.LoggerFactory /** @@ -34,9 +35,10 @@ import org.slf4j.LoggerFactory * cumbersome operation is rarely necessary on user-facing code. * */ -class TagSet private(private val _tags: Map[String, Any]) { +class TagSet private(private val _underlying: UnifiedMap[String, Any]) { import TagSet.withPair + /** * Creates a new TagSet instance that includes the provided key/value pair. If the provided key was already associated * with another value then the previous value will be discarded and overwritten with the provided one. @@ -67,7 +69,7 @@ class TagSet private(private val _tags: Map[String, Any]) { * overwritten with the provided one. */ def withTags(other: TagSet): TagSet = - new TagSet(_tags ++ other._tags) + and(other) /** @@ -99,22 +101,26 @@ class TagSet private(private val _tags: Map[String, Any]) { * instance are associated to a key present on the provided instance then the previous value will be discarded and * overwritten with the provided one. */ - def and(other: TagSet): TagSet = - new TagSet(_tags ++ other._tags) + def and(other: TagSet): TagSet = { + val mergedMap = new UnifiedMap[String, Any](other._underlying.size() + this._underlying.size()) + mergedMap.putAll(this._underlying) + mergedMap.putAll(other._underlying) + new TagSet(mergedMap) + } /** * Returns whether this TagSet instance does not contain any tags. */ def isEmpty(): Boolean = - _tags.isEmpty + _underlying.isEmpty /** * Returns whether this TagSet instance contains any tags. */ def nonEmpty(): Boolean = - _tags.nonEmpty + !_underlying.isEmpty /** @@ -122,7 +128,7 @@ class TagSet private(private val _tags: Map[String, Any]) { * built-in lookups on the [[Lookups]] companion object for more information. */ def get[T](lookup: Lookup[T]): T = - lookup.execute(_tags) + lookup.execute(_storage) /** @@ -132,14 +138,19 @@ class TagSet private(private val _tags: Map[String, Any]) { * * The returned sequence contains immutable values and is safe to share across threads. */ - def all(): Seq[Tag] = - _tags.foldLeft(List.empty[Tag]) { - case (ts, (key, value)) => value match { - case v: String => new immutable.String(key, v) :: ts - case v: Boolean => new immutable.Boolean(key, v) :: ts - case v: Long => new immutable.Long(key, v) :: ts + def all(): Seq[Tag] = { + var tags: List[Tag] = Nil + + _underlying.forEach(new BiConsumer[String, Any] { + override def accept(key: String, value: Any): Unit = value match { + case v: String => tags = new TagSet.immutable.String(key, v) :: tags + case v: Boolean => tags = new TagSet.immutable.Boolean(key, v) :: tags + case v: Long => tags = new TagSet.immutable.Long(key, v) :: tags } - } + }) + + tags + } /** @@ -149,59 +160,62 @@ class TagSet private(private val _tags: Map[String, Any]) { * structure that will be sent to the external systems. */ def iterator(): Iterator[Tag] = new Iterator[Tag] { - private val _entriesIterator = _tags.iterator - private var _longTag: mutable.Long = null - private var _stringTag: mutable.String = null - private var _booleanTag: mutable.Boolean = null + private val _entriesIterator = _underlying.keyValuesView().iterator() + private var _longTag: TagSet.mutable.Long = null + private var _stringTag: TagSet.mutable.String = null + private var _booleanTag: TagSet.mutable.Boolean = null override def hasNext: Boolean = _entriesIterator.hasNext override def next(): Tag = { - val (key, value) = _entriesIterator.next() - value match { - case v: String => stringTag(key, v) - case v: Boolean => booleanTag(key, v) - case v: Long => longTag(key, v) + val pair = _entriesIterator.next() + pair.getTwo match { + case v: String => stringTag(pair.getOne, v) + case v: Boolean => booleanTag(pair.getOne, v) + case v: Long => longTag(pair.getOne, v) } } private def stringTag(key: JString, value: JString): Tag.String = if(_stringTag == null) { - _stringTag = new mutable.String(key, value) + _stringTag = new TagSet.mutable.String(key, value) _stringTag } else _stringTag.updated(key, value) private def booleanTag(key: JString, value: JBoolean): Tag.Boolean = if(_booleanTag == null) { - _booleanTag = new mutable.Boolean(key, value) + _booleanTag = new TagSet.mutable.Boolean(key, value) _booleanTag } else _booleanTag.updated(key, value) private def longTag(key: JString, value: JLong): Tag.Long = if(_longTag == null) { - _longTag = new mutable.Long(key, value) + _longTag = new TagSet.mutable.Long(key, value) _longTag } else _longTag.updated(key, value) } - override def equals(other: Any): Boolean = - other != null && other.isInstanceOf[TagSet] && other.asInstanceOf[TagSet]._tags == this._tags + other != null && other.isInstanceOf[TagSet] && other.asInstanceOf[TagSet]._underlying == _underlying + override def hashCode(): Int = + _underlying.hashCode() override def toString: JString = { val sb = new StringBuilder() sb.append("Tags{") var hasTags = false - _tags.foreach { case (k, v) => + val iterator = _underlying.keyValuesView().iterator() + while(iterator.hasNext) { + val pair = iterator.next() if(hasTags) sb.append(",") - sb.append(k) + sb.append(pair.getOne) .append("=") - .append(v) + .append(pair.getTwo) hasTags = true } @@ -209,36 +223,104 @@ class TagSet private(private val _tags: Map[String, Any]) { sb.append("}").toString() } - private object immutable { - case class String(key: JString, value: JString) extends Tag.String - case class Boolean(key: JString, value: JBoolean) extends Tag.Boolean - case class Long(key: JString, value: JLong) extends Tag.Long + private val _storage = new TagSet.Storage { + override def get(key: String): Any = _underlying.get(key) + override def iterator(): Iterator[Tag] = TagSet.this.iterator() + override def isEmpty(): Boolean = _underlying.isEmpty } +} - private object mutable { - case class String(var key: JString, var value: JString) extends Tag.String with Updateable[JString] - case class Boolean(var key: JString, var value: JBoolean) extends Tag.Boolean with Updateable[JBoolean] - case class Long(var key: JString, var value: JLong) extends Tag.Long with Updateable[JLong] +object TagSet { - trait Updateable[T] { - var key: JString - var value: T + /** + * Describes a strategy to lookup values from a TagSet instance. Implementations of this interface will be provided + * with the actual data structure containing the tags and must perform any necessary runtime type checks to ensure + * that the returned value is in assignable to the expected type T. + * + * Several implementation are provided in the Lookup companion object and it is recommended to import and use those + * definitions when looking up keys from a Tags instance. + */ + trait Lookup[T] { + + /** + * Tries to find a value on a TagSet.Storage and returns a representation of it. In some cases the stored object + * might be returned as-is, in some others it might be transformed or wrapped on Option/Optional to handle missing + * values. Take a look at the Lookups companion object for examples.. + */ + def execute(storage: TagSet.Storage): T + } + + + /** + * A temporary structure that accumulates key/value and creates a new TagSet instance from them. It is faster to use + * a Builder and add tags to it rather than creating TagSet and add each key individually. Builder instances rely on + * internal mutable state and are not thread safe. + */ + trait Builder { + + /** Adds a new key/value pair to the builder. */ + def add(key: String, value: Any): Builder + + /** Creates a new TagSet instance that includes all valid key/value pairs added to this builder. */ + def create(): TagSet + } + + + /** + * Abstracts the actual storage used for a TagSet. This interface resembles a stripped down interface of an immutable + * map of String to Any, used to expose the underlying structure where tags are stored to Lookups, without leaking + * the actual implementation. + */ + trait Storage { + + /** + * Gets the value associated with the provided key, or null if no value was found. The decision of returning null + * when the key is not present is a conscious one, backed by the fact that users will never be exposed to this + * storage interface and they can decide their way of handling missing values by selecting an appropriate lookup. + */ + def get(key: String): Any + + /** + * Provides an Iterator that can go through all key/value pairs contained in the Storage instance. + */ + def iterator(): Iterator[Tag] + + /** + * Returns true if there are no tags in the storage. + */ + def isEmpty(): Boolean - def updated(key: JString, value: T): this.type = { - this.key = key - this.value = value - this - } - } } -} -object TagSet { /** * A valid instance of tags that doesn't contain any pairs. */ - val Empty = new TagSet(Map.empty.withDefaultValue(null)) + val Empty = new TagSet(UnifiedMap.newMap[String, Any]()) + + + /** + * Creates a new Builder instance. + */ + def builder(): Builder = new Builder { + private var _tagCount = 0 + private var _tags: List[(String, Any)] = Nil + + override def add(key: String, value: Any): Builder = { + if(isValidPair(key, value)) { + _tagCount += 1 + _tags = (key -> value) :: _tags + } + + this + } + + override def create(): TagSet = { + val newMap = new UnifiedMap[String, Any](_tagCount) + _tags.foreach { case (key, value) => newMap.put(key, value) } + new TagSet(newMap) + } + } /** @@ -266,8 +348,12 @@ object TagSet { * Constructs a new TagSet instance from a Map. The returned TagSet will only contain the entries that have String, * Long or Boolean values from the supplied map, any other entry in the map will be ignored. */ - def from(map: Map[String, Any]): TagSet = - new TagSet(map.filter { case (k, v) => isValidPair(k, v) } withDefaultValue(null)) + def from(map: Map[String, Any]): TagSet = { + val unifiedMap = new UnifiedMap[String, Any](map.size) + map.foreach { pair => if(isValidPair(pair._1, pair._2)) unifiedMap.put(pair._1, pair._2)} + + new TagSet(unifiedMap) + } /** @@ -275,22 +361,25 @@ object TagSet { * Long or Boolean values from the supplied map, any other entry in the map will be ignored. */ def from(map: java.util.Map[String, Any]): TagSet = { - val allowedTags = Map.newBuilder[String, Any] - map.entrySet() - .iterator() - .asScala - .foreach(e => if(isValidPair(e.getKey, e.getValue)) allowedTags += (e.getKey -> e.getValue)) + val unifiedMap = new UnifiedMap[String, Any](map.size) + map.forEach(new BiConsumer[String, Any] { + override def accept(key: String, value: Any): Unit = + if(isValidPair(key, value)) unifiedMap.put(key, value) + }) - new TagSet(allowedTags.result().withDefaultValue(null)) + new TagSet(unifiedMap) } private val _logger = LoggerFactory.getLogger(classOf[TagSet]) private def withPair(parent: TagSet, key: String, value: Any): TagSet = - if(isValidPair(key, value)) - new TagSet(parent._tags.updated(key, value)) - else + if(isValidPair(key, value)) { + val mergedMap = new UnifiedMap[String, Any](parent._underlying.size() + 1) + mergedMap.putAll(parent._underlying) + mergedMap.put(key, value) + new TagSet(mergedMap) + } else parent private def isValidPair(key: String, value: Any): Boolean = { @@ -313,16 +402,26 @@ object TagSet { private def isAllowedTagValue(v: Any): Boolean = v != null && (v.isInstanceOf[String] || v.isInstanceOf[Boolean] || v.isInstanceOf[Long]) + private object immutable { + case class String(key: JString, value: JString) extends Tag.String + case class Boolean(key: JString, value: JBoolean) extends Tag.Boolean + case class Long(key: JString, value: JLong) extends Tag.Long + } - /** - * Describes a strategy to lookup values from a TagSet instance. Implementations of this interface will be provided - * with the actual data structure containing the tags and must perform any necessary runtime type checks to ensure - * that the returned value is in assignable to the expected type T. - * - * Several implementation are provided in the Lookup companion object and it is recommended to import and use those - * definitions when looking up keys from a Tags instance. - */ - trait Lookup[T] { - def execute(storage: Map[String, Any]): T + private object mutable { + case class String(var key: JString, var value: JString) extends Tag.String with Updateable[JString] + case class Boolean(var key: JString, var value: JBoolean) extends Tag.Boolean with Updateable[JBoolean] + case class Long(var key: JString, var value: JLong) extends Tag.Long with Updateable[JLong] + + trait Updateable[T] { + var key: JString + var value: T + + def updated(key: JString, value: T): this.type = { + this.key = key + this.value = value + this + } + } } } \ No newline at end of file -- cgit v1.2.3