Use APM in a Windows Service (non .net core)

Hey, I've been trying to find an answer to this in the forum but I'm not able to find anything relevant.

I want to be able to create transaction for different methods in our code.
Filtering data/db transactions etc...

The application is an standard windows service application.

If I understand correctly I need to use the Agent Public API?
I've tried to configure it but I can't seem to get it right.

Anyone here that has done this or is it impossible?
We have several windows services in our team that we want to do this for.
Some of them are TopShelf services if that makes any difference.

BR
Frutty

Hi @frutty,

If I understand correctly I need to use the Agent Public API?

Correct, in this case all you need to do is to reference the Elastic.APM package and use the Public Agent API to create transactions in your code.

Anyone here that has done this or is it impossible?

That should be possible - what you describe here is definitely a use-case that should work.

What is exactly the issue? Have you already added code to capture a transaction?

GregKalapos
Yes I've already added the code for the transactions.
The issue is how should I configure the APM agent?
For the application I'm working with now we have our APM settings in our app.config.

And it just hit me, is the Public Agent Api available for ECE?

The issue is how should I configure the APM agent?

You can use environment variables. If you look at the list of our configs then for every config setting you'll have a column with "Environment variable name" - e.g. to set the APM Server url the environment variable name is ELASTIC_APM_SERVER_URLS. Just make sure that the process with the agent can read this environment variable.

For the application I'm working with now we have our APM settings in our app.config.

Unfortunately if you only use the Public Agent API the agent won't read app.config - it'll only work with environment variables. You could extend the agent to read app.config, but that needs some coding. Wouldn't just using environment variables be enough?

And it just hit me, is the Public Agent Api available for ECE?

Yes, you can basically use it anywhere - once you have an APM Server that sends the data to Elasticsearch, all parts of the agent should work. ECE should be also ok.

Thanks for the reply.

Is there an code example of this somewhere?

I will try to use the environment variables and see if I get that to work.

Hi @frutty

Is there an code example of this somewhere?

Code example of what part?

If you refer to my comment on reading the configs from a source which is not environment variables then this .cs file is a good place to look at.

If you generally need code samples on how to use the public agent API, I'd say the official doc is the best place.

We also have some small code snippets here and here.

@GregKalapos Thanks again for the response.

The first .cs file helps but isn't there any code example of how to set up the agent?

Maybe I'm stupid here but I'm not quite sure on which properties to set on which object.
I just have 4 properties I want to assign my APM Agent:

  • ServerUrls
  • ServiceName
  • TransactionSampleRate
  • SecretToken

When I try to use the Elastic.Apm.Agent.Setup() method it needs a ApmLogger/IConfigurationReader/IPayLoadSender.

I can't find any documentation on how to use this method. Which I assume is the one that I should use in order to configure the APM Agent?

BR
Frutty

EDIT: It just hit me, is the intented way to implement each of the interfaces?....

EDIT 2: And to use it like this?

public class ApmConfigurationReader : IConfigurationReader
{
    public ApmConfigurationReader()
    {            
        ServiceName = ConfigurationManager.AppSettings["elasticapm.ServiceName"].Trim();
        ServerUrls = new List<Uri> { new Uri(ConfigurationManager.AppSettings["elasticapm.ServerUrls"].Trim()) };
        TransactionSampleRate = double.Parse(ConfigurationManager.AppSettings["elasticapm.TransactionSampleRate"].Trim());
        SecretToken = ConfigurationManager.AppSettings["elasticapm.SecretToken"].Trim();
    }

    public string CaptureBody { get; }
    public List<string> CaptureBodyContentTypes { get; }
    public bool CaptureHeaders { get; }
    public bool CentralConfig { get; }
    public IReadOnlyList<WildcardMatcher> DisableMetrics { get; }
    public string Environment { get; }
    public TimeSpan FlushInterval { get; }
    public IReadOnlyDictionary<string, string> GlobalLabels { get; }
    public LogLevel LogLevel { get; }
    public int MaxBatchEventCount { get; }
    public int MaxQueueEventCount { get; }
    public double MetricsIntervalInMilliseconds { get; }
    public IReadOnlyList<WildcardMatcher> SanitizeFieldNames { get; }
    public string SecretToken { get; }
    public string ApiKey { get; }
    public IReadOnlyList<Uri> ServerUrls { get; }
    public string ServiceName { get; }
    public string ServiceNodeName { get; }
    public string ServiceVersion { get; }
    public double SpanFramesMinDurationInMilliseconds { get; }
    public int StackTraceLimit { get; }
    public int TransactionMaxSpans { get; }
    public double TransactionSampleRate { get; }
    public bool UseElasticTraceparentHeader { get; }
    public bool VerifyServerCert { get; }
}

//In program.cs
Agent.Setup(new AgentComponents(null, new ApmConfigurationReader(), null));

EDIT 3: Just tried this.
The Apm.Agent.Setup() relies on assembly System.Net.Http version 4.2.0.0 and I've tried to install several versions of the System.Net.Http nuget but with zero success.

@frutty you can customize the agent by passing custom components to it with Apm.Agent.Setup(), but that's optional.

If you don't call Setup(), the agent will read configs from environment variable. For all configs you pointed out in your question (ServerUrls, ServiceName, etc.), you can find the corresponding environment variable in this list (just click on each and it'll lead you to a page with the info about the setting including the environment variable names).

So unless there is a good reason to pass custom components to the agent (like a custom config reader) I'd not bother with that. Also, it's ok to just set the environment variable in C# when your service starts - this is an easy workaround I see people sometimes do.

I am also struggling with the same issue for how to setup Agent for a TopShelf .NET Core Windows service using EF Core. Do I need to use Elastic.Apm.NetCoreAll or Elastic.Apm package? Of which Object shall I set the Environment Variable? You have given good example of doing it in WEB API however it is missing for Windows Service.
Need to know what is the use of this method in your Public API sample - PassDistributedTracingData. Why it is only setting Service Name? Do I need to set others like Server Url, etc. in this function. How can I use this function and I put the transaction on my start of the process however, don't know how and where to setup the Agent. Please provide some working code sample.
public override void Work()
{
var transaction = Agent.Tracer.StartTransaction("DataUploadServiceTransaction", "DataUploadService");
try
{
Process();
}
catch(Exception ex)
{
transaction.CaptureException(ex);
}
finally
{
transaction.End();
}
}

Hi @sachinp200,

Please take a look at our documentation - all this should be covered there.

Let me help a bit with finding the right parts for your question:

Do I need to use Elastic.Apm.NetCoreAll or Elastic.Apm package?

This is covered here - below the code sample.

Of which Object shall I set the Environment Variable?

This part explains this.

You have given good example of doing it in WEB API however it is missing for Windows Service.

That should not change this - config reading is the same on both.

Need to know what is the use of this method in your Public API sample - PassDistributedTracingData

The DistributedTracingData parameter is documented here

Why it is only setting Service Name?

I assume you mean this line - as some of the doc above describes you can start the agent without setting any setting, in those case the agent starts with default values - it sends data to an APM server on localhost and sets the service name to the name of the entry assembly. The Public API sample is a bit special in this regard, because it starts the same assembly (executable in this case) twice to showcase distributed tracing - but given it's the same assembly it'd have the same service name. In order to avoid this, we change the service name from default to a specific one before we start the app the 2. time, this way it'll be 2 different services which is needed to better showcase distributed tracing.

Do I need to set others like Server Url, etc. in this function. How can I use this function and I put the transaction on my start of the process however, don't know how and where to setup the Agent.

I think all those were covered in above links - if not let me know.

Thanks Greg, I have gone through those links already. I have tried setting up environment variables in the Windows and also through the following code however I am not getting anything logged on Kibana APM module. I can successfully log for Web APIs. I am pasting my code below, let me know if this is not the way to do it and what is missing:
public override void Work()
{
Agent.Subscribe(new EfCoreDiagnosticsSubscriber());
var startInfo = new ProcessStartInfo();
startInfo.Environment["ELASTIC_APM_SERVICE_NAME"] = "DataUploadService";
startInfo.Environment["ELASTIC_APM_SERVER_URLS"] = "http://ec2-42-85-137-237.ap-southeast-2.compute.amazonaws.com:8200";
var outgoingDistributedTracingData = (Agent.Tracer.CurrentSpan?.OutgoingDistributedTracingData
?? Agent.Tracer.CurrentTransaction?.OutgoingDistributedTracingData)?.SerializeToString();
var transaction = Agent.Tracer.StartTransaction("DataUploadServiceTransaction", ApiConstants.ActionExec,
DistributedTracingData.TryDeserializeFromString(outgoingDistributedTracingData));
System.Diagnostics.Process.Start(startInfo);
try
{
MyProcess();
}
catch(Exception ex)
{
transaction.CaptureException(ex);
}
finally
{
transaction.End();
}
}

Do you really need to start a new process here?

I think the issue is this part:

var startInfo = new ProcessStartInfo();
startInfo.Environment["ELASTIC_APM_SERVICE_NAME"] = "DataUploadService";
startInfo.Environment["ELASTIC_APM_SERVER_URLS"] = "http://ec2-42-85-137-237.ap-southeast-2.compute.amazonaws.com:8200";

In the sample what you see we spawn a new process - that is what we use ProcessStartInfo for, but in your case probably you don't need that.

With ProcessStartInfo you don't set environment variables for your current process.

Instead, do this:

Environment.SetEnvironmentVariable("ELASTIC_APM_SERVICE_NAME", "DataUploadService");
Environment.SetEnvironmentVariable("ELASTIC_APM_SERVER_URLS", "http://ec2-42-85-137-237.ap-southeast-2.compute.amazonaws.com:8200";);

Also, just to be on the safe side, I'd suggest moving these lines before any other agent code.

This time my code looks like this:
using Elastic.Apm;
using Elastic.Apm.Api;

public class DataJob : Job
{
private readonly IDataService _DataService;
public DataJob(JobSetting setting, ILogger logger, IDataService DataService)
: base(setting, logger)
{
_DataService = DataService;
}

    public override void Work()
    {
        Environment.SetEnvironmentVariable("ELASTIC_APM_SERVICE_NAME", "DataUploadService");
        Environment.SetEnvironmentVariable("ELASTIC_APM_SERVER_URLS", "http://ec2-42-85-137-237.ap-southeast-2.compute.amazonaws.com:8200");
        Agent.Subscribe(new EfCoreDiagnosticsSubscriber());
       
        var outgoingDistributedTracingData = (Agent.Tracer.CurrentSpan?.OutgoingDistributedTracingData
            ?? Agent.Tracer.CurrentTransaction?.OutgoingDistributedTracingData)?.SerializeToString();            
        var transaction = Agent.Tracer.StartTransaction("DataUploadServiceTransaction", ApiConstants.ActionExec,
                DistributedTracingData.TryDeserializeFromString(outgoingDistributedTracingData));
       
        try
        {
            Process();
        }
        catch(Exception ex)
        {
            transaction.CaptureException(ex);
        }
        finally
        {
            transaction.End();
        }            
    }

    private void Process()
    {
        LoadExcelDataToStagingTable();                      
    }

}

But still nothing logged on the Kibana APM module.

@sachinp200 That APM Server seems to run in Elastic Cloud, is that right? In that case you'll also set the SecretToken. Can that be the missing part?

Yes Greg, it is running in cloud and I didn't use secret token for Web APIs and they are loggin data. BTW, can also use your help in knowing how to generate the token.

BTW, can also use your help in knowing how to generate the token.

If you go into your deployment and click to "APM" you can see the "
APM Server secret token" - there is also a reset button for it.

I didn't use secret token for Web APIs and they are loggin data.

Hmm... that's strange. Unless you changed something I think it should not work, but I'm not totally sure what's happening there.

I'd suggest looking into your APM info on cloud and try it with the secret token - I suspect it'd solve this, if not, we can look into other options.

Hi Greg, I will try the secret token part and get back to you if it doesn't work. However, I got it working now without secret token. Thanks for your help.

This topic was automatically closed 20 days after the last reply. New replies are no longer allowed.