Dotnet diagnostics / open telemetry on CI in 6 simple steps

Prerequisites : Having Jaeger / Zipkin for visualization . The easy way for installing Jaeger is to have Jaeger all in one ( either via Docker, either run on PC)

 

Step 1: Modify the csproj to add this

<ItemGroup>
  <PackageReference Include="OpenTelemetry" Version="1.1.0-beta1" />
    <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.0.0-rc3" />
    <PackageReference Include="OpenTelemetry.Exporter.Jaeger" Version="1.1.0-beta1" />
    <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.0.0-rc3" />
    <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.0.0-rc3" />
    <PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.0.0-rc3" />
  
    <AdditionalFiles Include="../AutoMethod.txt" />
    <PackageReference Include="AOPMethodsCommon" Version="2021.5.17.2230" />
    <PackageReference Include="AOPMethodsGenerator" Version="2021.5.17.2230" />

  </ItemGroup>


Optional : To see the RSCG files, add also

  <PropertyGroup>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
  </PropertyGroup>

Step 2:

2.1  If  ASP.NET Core  app,  add the following to Startup.cs

services.AddOpenTelemetryTracing(b =>
{
    string nameAssemblyEntry = Assembly.GetEntryAssembly().GetName().Name;
    var rb = ResourceBuilder.CreateDefault();
    rb.AddService(nameAssemblyEntry);
    var data = new Dictionary<string, object>() {
                { "PC", Environment.MachineName } };
    data.Add("Exe", nameAssemblyEntry);
    rb.AddAttributes(data);
    _ = b
        .AddAspNetCoreInstrumentation(opt=>
        {
            opt.RecordException = true;
                        
        })
        .AddHttpClientInstrumentation(opt=>
        {
            opt.SetHttpFlavor = true;                        
        })
        .AddSqlClientInstrumentation(opt=>
        {
            opt.SetDbStatementForStoredProcedure = true;
            opt.SetDbStatementForText = true;
            opt.RecordException = true;
            opt.EnableConnectionLevelAttributes = true;
        })
        .AddSource("MySource")
        .SetResourceBuilder(rb)
        .AddJaegerExporter(c =>
        {
            //var s = Configuration.GetSection("Jaeger");

            //s.Bind(c);
            c.AgentHost = "localhost";//TODO: put the name of the PC with Jaeger
                        
        });


})
    ;

2.2 If .NET Core executable , add the following code to main.cs


string nameAssemblyEntry = Assembly.GetEntryAssembly().GetName().Name;
var rb = ResourceBuilder.CreateDefault();
rb.AddService(nameAssemblyEntry);
var data = new Dictionary<string, object>() {
{ "PC", Environment.MachineName } };
data.Add("Exe", nameAssemblyEntry);
rb.AddAttributes(data);

openTelemetry = Sdk.CreateTracerProviderBuilder()
.AddSource("MySource")
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation()
.SetResourceBuilder(rb)
.AddJaegerExporter(o =>
 {
   //var s = Configuration.GetSection("Jaeger");

   //s.Bind(c);
   c.AgentHost = "localhost";//TODO: put the name of the PC with Jaeger
 })
.Build();

 

Step 3: Create near the .sln the file AutoMetods.ps1 with the following contents

function ReplaceWithAuto([string]$m )
{
$m=$m.Trim();
$firstpart= $method
$secondPart =""
#Write-Host $method
# do it fast now. Find first ( , name method is before that
#( could be done better: find from the last : add 1when found ( , add -1 when found ), stop at 0 )
# find first space before
$nr = ([regex]::Matches($m, [regex]::Escape(")") )).count
if($nr -gt 1){

$characters = [char[]]$method;
$nrOpen=0;
$iterator= $characters.Length;
do{
$iterator--;
if($characters[$iterator] -eq '('){
$nrOpen++;
}

if($characters[$iterator] -eq ')'){
$nrOpen--;
}

}while(($iterator -gt 0) -and ($nrOpen -ne 0))
$firstpart = $m.Substring(0,$iterator).Trim()
#return $firstpart
$spaceArr= $firstpart.Split(' ');
$firstpart =$spaceArr[$spaceArr.Length-1]
#return $firstpart

}
$arr = $firstpart.Split("(");
#Write-Host $arr[0]
$splitSpace = $arr[0].Split(" ")
$nameMethod = $splitSpace[$splitSpace.Length-1]
#Write-Host $nameMethod
[regex]$pattern = $nameMethod
#$method= ($method -replace [regex]::Escape($nameMethod) ,("auto"+$nameMethod))
$method= $pattern.Replace($method,("auto"+$nameMethod),1)
#Write-Host $method
return $method

}

function VerifyFolder()
{
Write-Host "starting"
$fileName = Get-ChildItem "*.cs" -Recurse
#$varItem = [regex]::Escape( "AOPMarkerMethod")
$varItem = "AOPMarkerMethod"
$filename | %{

$fileContent = gc $_
$x= ($fileContent -imatch "AOPMarkerMethod" )
#Write-Host $x.Length
if($x.Length -gt 0){
Write-Host $_.fullname
$LineNumbers =@( Select-String $_ -Pattern $varItem| Select-Object -ExpandProperty 'LineNumber')
Write-Host "found " $LineNumbers
Foreach ($LineNumber in $LineNumbers){
$method = $fileContent[$LineNumber]

Write-Host "Line where string is found: "$LineNumber
Write-Host $method

#Write-Host ReplaceWithAuto $method
$fileContent[$LineNumber] = ReplaceWithAuto($method)

Write-Host $fileContent[$LineNumber]
}

$fileContent |Set-Content $_.fullname

}
#(gc $_) -replace $varItem,"auto" |Set-Content $_.fullname

}
}

VerifyFolder

Step 4 : Create near the .sln Automethod.txt with the following content:

//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.CodeDom.Compiler;
using System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Linq;
using OpenTelemetry.Trace;
namespace {{NamespaceName}} {

[GeneratedCode("AOPMethods", "{{version}}")]
[CompilerGenerated]
public partial class {{ClassName}}{

public static readonly ActivitySource MyActivitySource = new ActivitySource("MySource");

{{~ for mi in Methods ~}}
//{{mi.Name}}
{{

strAwait = ""
strAsync =""
if mi.CouldUseAsync == true
strAwait = " await "
strAsync = " async "
end
separator = ""
if(mi.NrParameters > 0)
separator = ","
end
returnString = ""
if mi.CouldReturnVoidFromAsync == false
returnString = " return "
end
}}
public {{strAsync}} {{mi.ReturnType}} {{mi.NewName}} ({{mi.parametersDefinitionCSharp }}
{{separator}}
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0){
var sw=Stopwatch.StartNew();
using var span = MyActivitySource.StartActivity("{{mi.NewName}}", ActivityKind.Producer);

span.Start();

//parameters

{{~ for miArg in mi.Parameters ~}}
var val{{ miArg.Key }} = System.Text.Json.JsonSerializer.Serialize({{ miArg.Key }});
span.SetTag("Argument_{{ miArg.Key }}",val{{ miArg.Key }});
{{ end}}

span.SetTag("method","{{mi.NewName}}");
span.SetTag("called from ",memberName);
span.SetTag("called from file",sourceFilePath);
span.SetTag("called from line",sourceLineNumber);
var utcTime =System.DateTime.UtcNow;
span.SetTag("UTCDate",utcTime.ToString("yyyyMMddHHmmss"));
var userEmail = ((System.Security.Claims.ClaimsPrincipal)System.Threading.Thread.CurrentPrincipal)?.Claims?.Where(c => c.Type == "email")?.SingleOrDefault()?.Value;
userEmail ??= "NoUserConfigured";
span.SetTag("user",userEmail);
span.SetStatus(Status.Ok);
try{
Console.WriteLine("--{{mi.Name}} start ");
{{returnString}} {{ strAwait }} {{mi.Name}}({{ mi.parametersCallCSharp }});
}
catch(Exception ex){
Console.WriteLine("--{{mi.Name}} exception ");
span?.RecordException(ex);
span.SetStatus(Status.Error);
throw;
}
finally{
Console.WriteLine("--{{mi.Name}} finally ");
span.SetTag("ElapsedMilliseconds",sw.Elapsed.TotalMilliseconds);
span.Stop();
//span.Duration= sw.Elapsed.TotalMilliseconds;

}
}//end {{mi.NewName}}

{{ end}}
}
}

Step 5: Decorate your method with [AOPMarkerMethod]  ( namespace AOPMethodsCommon )

Something like this


[AOPMarkerMethod]
private NewCountry[] ArrangeCountriesSearch(NewCountry[] data)

And in the class definition
[AutoMethods(template = TemplateMethod.CustomTemplateFile,CustomTemplateFileName ="Automethod.txt", MethodSuffix = "bup")]

Step 6: Modify your CI to run

pwsh AutoMetods.ps1

or

powershell AutoMetods.ps1

 

 

Enjoy, that will be all !