-
-
Notifications
You must be signed in to change notification settings - Fork 95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Opentelemetry #497
base: main
Are you sure you want to change the base?
WIP: Opentelemetry #497
Changes from 2 commits
cfbf53d
524d15d
ec81a94
d087431
94fe329
b7bde26
25194a1
4d32e4b
41848d9
2a952bf
d3db9f7
40068a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,13 +2,42 @@ module Main | |
|
||
open Expecto | ||
open Expecto.Logging | ||
open OpenTelemetry.Resources | ||
open OpenTelemetry | ||
open OpenTelemetry.Trace | ||
open System.Threading | ||
open System.Diagnostics | ||
|
||
let serviceName = "Expecto.Tests" | ||
|
||
let logger = Log.create serviceName | ||
|
||
|
||
let resourceBuilder = | ||
ResourceBuilder | ||
.CreateDefault() | ||
.AddService(serviceName = serviceName) | ||
|
||
|
||
let logger = Log.create "Expecto.Tests" | ||
|
||
[<EntryPoint>] | ||
let main args = | ||
let activitySource = new ActivitySource(serviceName) | ||
use traceProvider = | ||
Sdk | ||
.CreateTracerProviderBuilder() | ||
.AddSource(serviceName) | ||
.SetResourceBuilder(resourceBuilder ) | ||
.AddOtlpExporter() | ||
.Build() | ||
let tracer = traceProvider.GetTracer(serviceName) | ||
// use span = tracer.StartActiveSpan("Expecto.main") | ||
use span = tracer.StartRootSpan("Expecto.main") | ||
let test = | ||
Impl.testFromThisAssembly() | ||
|> Option.orDefault (TestList ([], Normal)) | ||
|> Test.shuffle "." | ||
runTestsWithCLIArgs [NUnit_Summary "bin/Expecto.Tests.TestResults.xml"] args test | ||
runTestsWithCLIArgs [NUnit_Summary "bin/Expecto.Tests.TestResults.xml"; CLIArguments.ActivitySource activitySource] args test | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here a consumer would pass in the ActivitySource. Otherwise they'd have to add the dedicated Expecto one to the |
||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
FsCheck | ||
FsCheck | ||
OpenTelemetry.Exporter.OpenTelemetryProtocol |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
namespace Expecto | ||
|
||
open System | ||
open System.Collections.Generic | ||
open System.Diagnostics | ||
open System.Reflection | ||
open System.Threading | ||
|
@@ -9,6 +10,20 @@ open Expecto.Logging.Message | |
open Helpers | ||
open Mono.Cecil | ||
|
||
//! The other option is to use a dedicated activity source for Expecto instead of adding it to the config | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One option is to have Expecto have a dedicate ActivitySource, the other is to add it the the Expecto config and allow a user to pass it along. |
||
|
||
// module ActivitySource = | ||
|
||
// let [<Literal>] serviceName = "Expecto" // Should be public so consumers have a strong name when adding Sources | ||
// let private version = lazy ( | ||
// let assembly = typeof<FlatTest>.Assembly | ||
// let version = assembly.GetName().Version | ||
// version.ToString() | ||
// ) | ||
|
||
// let internal activitySource = lazy new ActivitySource(serviceName, version.Value) | ||
|
||
|
||
// TODO: make internal? | ||
module Impl = | ||
|
||
|
@@ -520,6 +535,11 @@ module Impl = | |
colour: ColourLevel | ||
/// Split test names by `.` or `/` | ||
joinWith: JoinWith | ||
// One option is to allow the consumer to provide an activity source | ||
// only problem is the only way to update the config is by using the CLIArguments currently | ||
// we would have to add a new CLIArgument but that doesn't really work as it's not a reallyCLI option | ||
// or have another way of updating the config after it's been created | ||
activitySource : ActivitySource option | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned, we can have an activity source in the config but there's not really a function that lets people update the config. It might be more tricky to do this option if people are using the |
||
} | ||
static member defaultConfig = | ||
{ runInParallel = true | ||
|
@@ -546,6 +566,7 @@ module Impl = | |
noSpinner = false | ||
colour = Colour8 | ||
joinWith = JoinWith.Dot | ||
activitySource = None | ||
} | ||
|
||
member x.appendSummaryHandler handleSummary = | ||
|
@@ -559,8 +580,63 @@ module Impl = | |
} | ||
} | ||
|
||
let inline internal setStatus (status : ActivityStatusCode) (span : Activity) = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lots of helper functions. Don't have to live here, just here for the moment. |
||
if isNull span |> not then | ||
span.SetStatus(status) |> ignore | ||
|
||
let inline internal setExn (e : exn) (span : Activity) = | ||
if isNull span |> not then | ||
let tags = | ||
ActivityTagsCollection( | ||
seq { | ||
KeyValuePair("exception.type", box (e.GetType().Name)) | ||
KeyValuePair("exception.stacktrace", box (e.ToString())) | ||
if not <| String.IsNullOrEmpty(e.Message) then | ||
KeyValuePair("exception.message", box e.Message) | ||
} | ||
) | ||
|
||
ActivityEvent("exception", tags = tags) | ||
|> span.AddEvent | ||
|> ignore | ||
|
||
let inline internal setExnMarkFailed (e : exn) (span : Activity) = | ||
if isNull span |> not then | ||
setExn e span | ||
span |> setStatus ActivityStatusCode.Error | ||
|
||
let setSourceLocation (sourceLoc : SourceLocation) (span : Activity) = | ||
if isNull span |> not && sourceLoc <> SourceLocation.empty then | ||
span.SetTag("code.lineno", sourceLoc.lineNumber) |> ignore | ||
span.SetTag("code.filepath", sourceLoc.sourcePath) |> ignore | ||
|
||
let inline internal addOutcome (result : TestResult) (span : Activity) = | ||
if isNull span |> not then | ||
span.SetTag("test.result.status", result.tag) |> ignore | ||
span.SetTag("test.result.message", result) |> ignore | ||
|
||
let inline internal start (span : Activity) = | ||
if isNull span |> not then | ||
span.Start() |> ignore | ||
span | ||
|
||
let inline internal stop (span : Activity) = | ||
if isNull span |> not then | ||
span.Stop() |> ignore | ||
|
||
let inline internal createActivity (name : string) (source : ActivitySource option) = | ||
match source with | ||
| Some source when not(isNull source) -> source.CreateActivity(name, ActivityKind.Internal) | ||
| _ -> null | ||
|
||
let execTestAsync (ct:CancellationToken) config (test:FlatTest) : Async<TestSummary> = | ||
async { | ||
let span = | ||
config.activitySource | ||
|> createActivity (config.joinWith.format test.name) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the starting point for creating an span. Later we add additional information to it based on whether we pass/fail/ignore. |
||
span |> setSourceLocation (config.locate test.test) | ||
|
||
use span = start span | ||
let w = Stopwatch.StartNew() | ||
try | ||
match test.shouldSkipEvaluation with | ||
|
@@ -593,32 +669,59 @@ module Impl = | |
) | ||
do! test fsConfig | ||
w.Stop() | ||
return TestSummary.single Passed (float w.ElapsedMilliseconds) | ||
stop span | ||
let result = Passed | ||
addOutcome result span | ||
setStatus ActivityStatusCode.Ok span | ||
return TestSummary.single result (float w.ElapsedMilliseconds) | ||
with | ||
| :? AssertException as e -> | ||
w.Stop() | ||
stop span | ||
let msg = | ||
"\n" + e.Message + "\n" + | ||
(e.StackTrace.Split('\n') | ||
|> Seq.skipWhile (fun l -> l.StartsWith(" at Expecto.Expect.")) | ||
|> Seq.truncate 5 | ||
|> String.concat "\n") | ||
return TestSummary.single (Failed msg) (float w.ElapsedMilliseconds) | ||
let result = Failed msg | ||
addOutcome result span | ||
setExnMarkFailed e span | ||
return TestSummary.single result (float w.ElapsedMilliseconds) | ||
| :? FailedException as e -> | ||
w.Stop() | ||
return TestSummary.single (Failed ("\n"+e.Message)) (float w.ElapsedMilliseconds) | ||
stop span | ||
let result = Failed ("\n"+e.Message) | ||
addOutcome result span | ||
setExnMarkFailed e span | ||
return TestSummary.single result (float w.ElapsedMilliseconds) | ||
| :? IgnoreException as e -> | ||
w.Stop() | ||
return TestSummary.single (Ignored e.Message) (float w.ElapsedMilliseconds) | ||
stop span | ||
let result = Ignored e.Message | ||
addOutcome result span | ||
setExn e span | ||
return TestSummary.single result (float w.ElapsedMilliseconds) | ||
| :? AggregateException as e when e.InnerExceptions.Count = 1 -> | ||
w.Stop() | ||
stop span | ||
if e.InnerException :? IgnoreException then | ||
return TestSummary.single (Ignored e.InnerException.Message) (float w.ElapsedMilliseconds) | ||
let result = Ignored e.InnerException.Message | ||
addOutcome result span | ||
setExn e span | ||
return TestSummary.single result (float w.ElapsedMilliseconds) | ||
else | ||
return TestSummary.single (Error e.InnerException) (float w.ElapsedMilliseconds) | ||
let result = Error e.InnerException | ||
addOutcome result span | ||
setExnMarkFailed e span | ||
return TestSummary.single result (float w.ElapsedMilliseconds) | ||
| e -> | ||
w.Stop() | ||
return TestSummary.single (Error e) (float w.ElapsedMilliseconds) | ||
stop span | ||
let result = Error e | ||
addOutcome result span | ||
setExnMarkFailed e span | ||
return TestSummary.single result (float w.ElapsedMilliseconds) | ||
} | ||
|
||
let private numberOfWorkers limit config = | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is adding the OpenTelemetry Exporter to the test project, mostly for demo purposes. Could create a separate project if desired.