-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- **Hide Hooks from public interface** - **Add TraceHooks** - **Add feature flag key attribute**
- Loading branch information
1 parent
b2488ec
commit cefad31
Showing
12 changed files
with
364 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
...ature/sdk-otel4s/src/main/scala/io/cardell/openfeature/otel4s/FeatureFlagAttributes.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/* | ||
* Copyright 2023 Alex Cardell | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package io.cardell.openfeature.otel4s | ||
|
||
import org.typelevel.otel4s.AttributeKey | ||
|
||
// Shim attributes until they are moved from experimental to stable | ||
object FeatureFlagAttributes { | ||
val FeatureFlagKey: AttributeKey[String] = AttributeKey("feature_flag.key") | ||
|
||
/** The name of the service provider that performs the flag evaluation. | ||
*/ | ||
val FeatureFlagProviderName: AttributeKey[String] = AttributeKey( | ||
"feature_flag.provider_name" | ||
) | ||
|
||
/** SHOULD be a semantic identifier for a value. If one is unavailable, a | ||
* stringified version of the value can be used. <p> | ||
* @note | ||
* <p> A semantic identifier, commonly referred to as a variant, provides a | ||
* means for referring to a value without including the value itself. This | ||
* can provide additional context for understanding the meaning behind a | ||
* value. For example, the variant `red` maybe be used for the value | ||
* `#c05543`. <p> A stringified version of the value can be used in | ||
* situations where a semantic identifier is unavailable. String | ||
* representation of the value should be determined by the implementer. | ||
*/ | ||
val FeatureFlagVariant: AttributeKey[String] = AttributeKey( | ||
"feature_flag.variant" | ||
) | ||
|
||
} |
91 changes: 91 additions & 0 deletions
91
openfeature/sdk-otel4s/src/main/scala/io/cardell/openfeature/otel4s/TraceHook.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
* Copyright 2023 Alex Cardell | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package io.cardell.openfeature.otel4s | ||
|
||
import cats.data.OptionT | ||
import cats.effect.IO | ||
import cats.effect.IOLocal | ||
import cats.syntax.all._ | ||
import org.typelevel.otel4s.Attributes | ||
import org.typelevel.otel4s.trace.Span | ||
import org.typelevel.otel4s.trace.StatusCode | ||
import org.typelevel.otel4s.trace.Tracer | ||
|
||
import io.cardell.openfeature.AfterHook | ||
import io.cardell.openfeature.BeforeHook | ||
import io.cardell.openfeature.ErrorHook | ||
import io.cardell.openfeature.FinallyHook | ||
import io.cardell.openfeature.Hook | ||
|
||
object TraceHooks { | ||
|
||
def ioLocal(implicit T: Tracer[IO]): IO[List[Hook[IO]]] = IOLocal( | ||
Option.empty[Span[IO]] | ||
).map(fromIOLocal) | ||
|
||
private def fromIOLocal( | ||
local: IOLocal[Option[Span[IO]]] | ||
)(implicit T: Tracer[IO]): List[Hook[IO]] = { | ||
import FeatureFlagAttributes._ | ||
|
||
val before = BeforeHook[IO] { case (context, _) => | ||
val attributes = Attributes( | ||
FeatureFlagKey(context.flagKey) | ||
) | ||
|
||
Tracer[IO] | ||
.span("resolve-flag", attributes) | ||
.startUnmanaged | ||
.flatMap(s => local.update(_ => s.some)) | ||
.as(None) | ||
} | ||
|
||
val after = AfterHook[IO] { case _ => | ||
OptionT(local.get) | ||
.semiflatMap { span => | ||
for { | ||
_ <- span.setStatus(StatusCode.Ok) | ||
_ <- span.end | ||
} yield () | ||
} | ||
.value | ||
.void | ||
} | ||
|
||
val error = ErrorHook[IO] { case (_, _, error) => | ||
OptionT(local.get) | ||
.semiflatMap { span => | ||
for { | ||
_ <- span.setStatus(StatusCode.Error) | ||
_ <- span.recordException(error) | ||
} yield () | ||
} | ||
.value | ||
.void | ||
} | ||
|
||
val finally_ = FinallyHook[IO] { case _ => | ||
OptionT(local.get) | ||
.semiflatMap(_.end) | ||
.value | ||
.void | ||
} | ||
|
||
List(before, after, error, finally_) | ||
} | ||
|
||
} |
131 changes: 131 additions & 0 deletions
131
openfeature/sdk-otel4s/src/test/scala/io/cardell/openfeature/otel4s/TraceHookTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
/* | ||
* Copyright 2023 Alex Cardell | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package io.cardell.openfeature.otel4s | ||
|
||
import cats.effect.IO | ||
import munit.CatsEffectSuite | ||
import org.typelevel.otel4s.sdk.testkit.trace.TracesTestkit | ||
import org.typelevel.otel4s.trace.StatusCode | ||
|
||
import io.cardell.openfeature.AfterHook | ||
import io.cardell.openfeature.EvaluationContext | ||
import io.cardell.openfeature.FlagValue | ||
import io.cardell.openfeature.provider.ProviderImpl | ||
import io.cardell.openfeature.provider.memory.MemoryProvider | ||
|
||
class TraceHookTest extends CatsEffectSuite { | ||
|
||
val setupProvider = MemoryProvider[IO]( | ||
Map("boolean-flag" -> FlagValue.BooleanValue(true)) | ||
) | ||
|
||
val setupTestkit = TracesTestkit.inMemory[IO]() | ||
|
||
test("span is applied") { | ||
val expectedFlagResult = true | ||
val expectedSpanName = "resolve-flag" | ||
val expectedSpanCount = 1 | ||
val expectedSpanStatus = StatusCode.Ok | ||
|
||
setupTestkit.use { kit => | ||
val setupTracer = kit.tracerProvider.tracer("name").get | ||
|
||
setupTracer.flatMap { implicit tracer => | ||
for { | ||
hooks <- TraceHooks.ioLocal | ||
provider <- setupProvider.map(ProviderImpl[IO]) | ||
hookedProvider = hooks.foldLeft(provider)(_ withHook _) | ||
flagResolution <- hookedProvider.resolveBooleanValue( | ||
"boolean-flag", | ||
false, | ||
EvaluationContext.empty | ||
) | ||
flagResult = flagResolution.value | ||
spans <- kit.finishedSpans | ||
spanCount = spans.size | ||
headSpan = spans.headOption | ||
spanName = headSpan.map(_.name) | ||
spanStatus = headSpan.map(_.status.status) | ||
spanEnded = headSpan.map(_.hasEnded) | ||
spanAttrs = headSpan.map(_.attributes.elements) | ||
flagKeyAttrExists = spanAttrs.map( | ||
_.exists(_ == FeatureFlagAttributes.FeatureFlagKey("boolean-flag")) | ||
) | ||
} yield { | ||
assertEquals(flagResult, expectedFlagResult) | ||
assertEquals(spanCount, expectedSpanCount) | ||
assertEquals(spanName, Some(expectedSpanName)) | ||
assertEquals(spanStatus, Some(expectedSpanStatus)) | ||
assertEquals(spanEnded, Some(true)) | ||
assertEquals(flagKeyAttrExists, Some(true)) | ||
} | ||
|
||
} | ||
} | ||
|
||
} | ||
|
||
test("span is recorded as exception when exception thrown in after hook") { | ||
val expectedSpanName = "resolve-flag" | ||
val expectedSpanCount = 1 | ||
val expectedSpanStatus = StatusCode.Error | ||
|
||
val throwingHook = AfterHook[IO] { case _ => | ||
IO.raiseError(new Throwable("throwing hook")) | ||
} | ||
|
||
setupTestkit.use { kit => | ||
val setupTracer = kit.tracerProvider.tracer("name").get | ||
|
||
setupTracer.flatMap { implicit tracer => | ||
for { | ||
traceHooks <- TraceHooks.ioLocal | ||
(before, others) = (traceHooks.head, traceHooks.tail) | ||
hooks = List(before, throwingHook) ++ others | ||
provider <- setupProvider.map(ProviderImpl[IO]) | ||
hookedProvider = hooks.foldLeft(provider)(_ withHook _) | ||
_ <- | ||
hookedProvider | ||
.resolveBooleanValue( | ||
"boolean-flag", | ||
false, | ||
EvaluationContext.empty | ||
) | ||
.attempt | ||
spans <- kit.finishedSpans | ||
spanCount = spans.size | ||
headSpan = spans.headOption | ||
spanName = headSpan.map(_.name) | ||
spanStatus = headSpan.map(_.status.status) | ||
spanEnded = headSpan.map(_.hasEnded) | ||
spanAttrs = headSpan.map(_.attributes.elements) | ||
flagKeyAttrExists = spanAttrs.map( | ||
_.exists(_ == FeatureFlagAttributes.FeatureFlagKey("boolean-flag")) | ||
) | ||
} yield { | ||
assertEquals(spanCount, expectedSpanCount) | ||
assertEquals(spanName, Some(expectedSpanName)) | ||
assertEquals(spanStatus, Some(expectedSpanStatus)) | ||
assertEquals(spanEnded, Some(true)) | ||
assertEquals(flagKeyAttrExists, Some(true)) | ||
} | ||
} | ||
} | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.