@@ -336,6 +336,89 @@ public void GenevaTraceExporter_Serialization_Success(bool hasTableNameMapping,
336336 }
337337 }
338338
339+ [ Fact ]
340+ public void GenevaTraceExporter_ServerSpan_HttpUrl_Success ( )
341+ {
342+ var path = string . Empty ;
343+ Socket server = null ;
344+ try
345+ {
346+ var invocationCount = 0 ;
347+ var exporterOptions = new GenevaExporterOptions ( ) ;
348+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
349+ {
350+ exporterOptions . ConnectionString = "EtwSession=OpenTelemetry" ;
351+ }
352+ else
353+ {
354+ path = GetRandomFilePath ( ) ;
355+ exporterOptions . ConnectionString = "Endpoint=unix:" + path ;
356+ var endpoint = new UnixDomainSocketEndPoint ( path ) ;
357+ server = new Socket ( AddressFamily . Unix , SocketType . Stream , ProtocolType . IP ) ;
358+ server . Bind ( endpoint ) ;
359+ server . Listen ( 1 ) ;
360+ }
361+
362+ using var exporter = new MsgPackTraceExporter ( exporterOptions ) ;
363+
364+ var m_buffer = exporter . Buffer ;
365+
366+ // Add an ActivityListener to serialize the activity and assert that it was valid on ActivityStopped event
367+
368+ // Set the ActivitySourceName to the unique value of the test method name to avoid interference with
369+ // the ActivitySource used by other unit tests.
370+ var sourceName = GetTestMethodName ( ) ;
371+
372+ using var listener = new ActivityListener ( ) ;
373+ listener . ShouldListenTo = ( activitySource ) => activitySource . Name == sourceName ;
374+ listener . Sample = ( ref ActivityCreationOptions < ActivityContext > options ) => ActivitySamplingResult . AllDataAndRecorded ;
375+ listener . ActivityStopped = ( activity ) =>
376+ {
377+ _ = exporter . SerializeActivity ( activity ) ;
378+ var fluentdData = MessagePack . MessagePackSerializer . Deserialize < object > ( m_buffer . Value , MessagePack . Resolvers . ContractlessStandardResolver . Options ) ;
379+ this . AssertHttpUrlForActivity ( exporterOptions , fluentdData , activity ) ;
380+ invocationCount ++ ;
381+ } ;
382+ ActivitySource . AddActivityListener ( listener ) ;
383+
384+ var source = new ActivitySource ( sourceName ) ;
385+
386+ // HTTP semconv: Combination of url.scheme, server.address, server.port, url.path and url.query
387+ // attributes for HTTP server spans.
388+ using ( var parent = source . StartActivity ( "HttpIn" , ActivityKind . Server ) )
389+ {
390+ parent . SetTag ( "http.request.method" , "GET" ) ;
391+ parent . SetTag ( "url.scheme" , "https" ) ;
392+ parent . SetTag ( "server.address" , "localhost" ) ;
393+ parent . SetTag ( "server.port" , 443 ) ;
394+ parent . SetTag ( "url.path" , "/wiki/Rabbit" ) ;
395+
396+ // HTTP semconv: url.full attribute for HTTP client spans.
397+ using ( var child = source . StartActivity ( "HttpOut" , ActivityKind . Client ) )
398+ {
399+ child . SetTag ( "http.request.method" , "GET" ) ;
400+ child . SetTag ( "url.full" , "https://www.wikipedia.org/wiki/Rabbit?id=7" ) ;
401+ child . SetTag ( "http.status_code" , 404 ) ;
402+ }
403+
404+ parent ? . SetTag ( "http.response.status_code" , 200 ) ;
405+ }
406+
407+ Assert . Equal ( 2 , invocationCount ) ;
408+ }
409+ finally
410+ {
411+ server ? . Dispose ( ) ;
412+ try
413+ {
414+ File . Delete ( path ) ;
415+ }
416+ catch
417+ {
418+ }
419+ }
420+ }
421+
339422 [ SkipUnlessPlatformMatchesFact ( TestPlatform . Linux ) ]
340423 public void GenevaTraceExporter_Constructor_Missing_Agent_Linux ( )
341424 {
@@ -778,4 +861,70 @@ private void AssertFluentdForwardModeForActivity(GenevaExporterOptions exporterO
778861
779862 customChecksForActivity ? . Invoke ( mapping ) ;
780863 }
864+
865+ private void AssertHttpUrlForActivity ( GenevaExporterOptions exporterOptions , object fluentdData , Activity activity )
866+ {
867+ /* Fluentd Forward Mode:
868+ [
869+ "Span",
870+ [
871+ [ <timestamp>, { "env_ver": "4.0", ... } ]
872+ ],
873+ { "TimeFormat": "DateTime" }
874+ ]
875+ */
876+
877+ var signal = ( fluentdData as object [ ] ) [ 0 ] as string ;
878+ var TimeStampAndMappings = ( ( fluentdData as object [ ] ) [ 1 ] as object [ ] ) [ 0 ] ;
879+ var timeStamp = ( DateTime ) ( TimeStampAndMappings as object [ ] ) [ 0 ] ;
880+ var mapping = ( TimeStampAndMappings as object [ ] ) [ 1 ] as Dictionary < object , object > ;
881+
882+ Assert . Equal ( ( byte ) activity . Kind , mapping [ "kind" ] ) ;
883+ var tags = activity . TagObjects . ToDictionary ( tag => tag . Key , tag => tag . Value ) ;
884+
885+ if ( activity . Kind == ActivityKind . Server )
886+ {
887+ // For HTTP server spans, they might contain these attributes for URL:
888+ // Unstable HTTP semconv: Combination of http.scheme, net.host.name, net.host.port, and http.target attributes.
889+ // Stable HTTP semconv: Combination of url.scheme, server.address, server.port, url.path and url.query attributes.
890+ // They will be mapped to httpUrl by Geneva exporter in MsgPackTraceExporter.
891+ Assert . DoesNotContain ( "http.scheme" , mapping . Keys ) ;
892+ Assert . DoesNotContain ( "net.host.name" , mapping . Keys ) ;
893+ Assert . DoesNotContain ( "net.host.port" , mapping . Keys ) ;
894+ Assert . DoesNotContain ( "http.target" , mapping . Keys ) ;
895+ Assert . DoesNotContain ( "url.scheme" , mapping . Keys ) ;
896+ Assert . DoesNotContain ( "server.address" , mapping . Keys ) ;
897+ Assert . DoesNotContain ( "server.port" , mapping . Keys ) ;
898+ Assert . DoesNotContain ( "url.path" , mapping . Keys ) ;
899+ Assert . DoesNotContain ( "url.query" , mapping . Keys ) ;
900+
901+ Assert . Equal ( "GET" , mapping [ "httpMethod" ] ) ;
902+ Assert . Equal ( "https://localhost:443/wiki/Rabbit" , mapping [ "httpUrl" ] ) ;
903+
904+ Assert . DoesNotContain ( "http.status_code" , mapping . Keys ) ;
905+ Assert . DoesNotContain ( "http.response.status_code" , mapping . Keys ) ;
906+ Assert . Equal ( 200 , Convert . ToInt32 ( mapping [ "httpStatusCode" ] ) ) ;
907+ }
908+ else if ( activity . Kind == ActivityKind . Client )
909+ {
910+ // For HTTP client spans, they might contain this attribute for URL:
911+ // Unstable HTTP semconv: http.url attribute.
912+ // Stable HTTP semconv: url.full attribute.
913+ // They will be mapped to httpUrl by Geneva exporter in MsgPackTraceExporter.
914+ Assert . DoesNotContain ( "http.url" , mapping . Keys ) ;
915+ Assert . DoesNotContain ( "url.full" , mapping . Keys ) ;
916+
917+ Assert . Equal ( "GET" , mapping [ "httpMethod" ] ) ;
918+
919+ Assert . Equal ( tags [ "url.full" ] , mapping [ "httpUrl" ] ) ;
920+
921+ Assert . DoesNotContain ( "http.status_code" , mapping . Keys ) ;
922+ Assert . DoesNotContain ( "http.response.status_code" , mapping . Keys ) ;
923+ Assert . Equal ( 404 , Convert . ToInt32 ( mapping [ "httpStatusCode" ] ) ) ;
924+ }
925+ else
926+ {
927+ throw new InvalidOperationException ( $ "Unexpected ActivityKind: { activity . Kind } . Expected either Server or Client.") ;
928+ }
929+ }
781930}
0 commit comments