Custom properties in monitoring and telemetry

Two years ago, I wrote a blog post Monitoring and telemetry in F&O where, among other things, I showed how to add a custom trace message and even include a custom property:

Map properties = new Map(Types::String, Types::String);
properties.add('Feature', 'My feature 1');
properties.add('MyCorrelationId', '123456');
 
SysGlobalTelemetry::logTraceWithCustomProperties('Custom message from X++', properties)

Microsoft keep evolving this area by adding more things that are logged automatically and the API has evolved as well. What I showed before is now deprecated and the new way looks like this:

SysApplicationInsightsTraceTelemetry telemetryData = SysApplicationInsightsTraceTelemetry::newFromMessageAndSeverity(
    'Credit note created for XYZ.', 
    Microsoft.ApplicationInsights.DataContracts.SeverityLevel::Information);
 
telemetryData.addProperty(MyBusinessProcessProperty::newFromValue("Sales invoice"));
 
SysApplicationInsightsTelemetryLogger::instance().trackTrace(telemetryData);

The biggest difference is that the property isn’t identified merely by its name anymore; we use a class. I think that Microsoft decided to do it this way because the class now includes an extra piece of information: compliance data type. It’s also possible to add more data or logic to these classes in future without changing the method of adding properties.

The example above uses MyBusinessProcessProperty class; this is how I’ve implemented it:

internal final class MyBusinessProcessProperty extends SysApplicationInsightsProperty
{
    private const str PropertyName = 'BusinessProcess';
 
    internal static MyBusinessProcessProperty newFromValue(str _value)
    {
        return new MyBusinessProcessProperty(_value);
    }
 
    protected container initialize()
    {
        return [PropertyName, SysApplicationInsightsComplianceDataType::SystemMetadata];
    }
}

By the way, I don’t like the new API too much. Ideally, adding trace message should be as simple as possible, so we can add tracing easily and we can focus on the actual business logic, not a big pile of code just for tracing.

For instance, the decision to expose the CLR enum (Microsoft.ApplicationInsights.DataContracts.SeverityLevel) doesn’t make a good sense to me. You either need use the full long name, or you need to interrupt your work, go to the top of the file, add a using statement and then continue. There should be a native X++ enum or a simply an appropriate method.

There are many ways how it could be redesigned. Let me show you a very simple extension making quite a difference.

Original code for adding a trace message (with a custom property):

SysApplicationInsightsTraceTelemetry telemetryData = SysApplicationInsightsTraceTelemetry::newFromMessageAndSeverity(
    'Credit note created for XYZ.', 
    Microsoft.ApplicationInsights.DataContracts.SeverityLevel::Information);
 
telemetryData.addProperty(MyBusinessProcessProperty::newFromValue("Sales invoice"));
 
SysApplicationInsightsTelemetryLogger::instance().trackTrace(telemetryData);

A new way:

SysApplicationInsightsTraceTelemetry::newInfo('Credit note created for XYZ.')
    .addProperty(MyBusinessProcessProperty::newFromValue("Sales invoice"))
    .send();

In this case, I’ve just added two methods to SysApplicationInsightsTraceTelemetry class:

using Microsoft.ApplicationInsights.DataContracts;
 
[ExtensionOf(classStr(SysApplicationInsightsTraceTelemetry))]
final class SysApplicationInsightsTraceTelemetry_My_Extension
{
    public static SysApplicationInsightsTraceTelemetry newInfo(str _traceMessage)
    {
        return SysApplicationInsightsTraceTelemetry::newFromMessageAndSeverity(_traceMessage, SeverityLevel::Information);
    }
 
    public void send()
    {
        SysApplicationInsightsTelemetryLogger::instance().trackTrace(this);
    }
}

Default fields in Open in Excel

When you open a data entity in Data Connector in Excel (usually from an F&O form by Open in Excel), not all fields must be displayed by default.

You can go to Designer and add more fields from Available to Selected.

But what if you want some fields included by default? You could create a template, but that’s a different topic.

I think that you get just the mandatory field by default, but you can define the list of selected fields (and their sequence) simply by putting them to AutoReport field group on the data entity.

Electronic reporting: Method returning a list of records

I had a scenario in electronic report where I wanted to reuse existing X++ code that generates some temporary table records for reporting purpose. Therefore I wanted electronic reporting to call my X++ method (for a particular context, an invoice in this case) and use the list of records returned by this method.

Electronic reporting supports method calls, but all the information I found on internet was about methods returning a single primitive value or a single record.

But it turned out it’s supported and actually quite easy to use.

The key is that the method must return on of the supported data types and it must be decorated with ERTableNameAttribute. Like this:

[ERTableName(tableStr(MyTable))]
public static RecordLinkList getData() { ... }

The supported data types are:

  • Query
  • RecordLinkList
  • RecordSortedList
  • any class implementing System.Collections.IEnumerable interface (.NET arrays, lists etc.)

If you wonder how I know it, I found the definition in ERDataContainerDescriptorBase.isRecordCollection().

One way of using such a method is defining a static method in class and using it the Class data source in ER.

Let me also extend the example with a parameter, to give the method some instructions about what it should generate:

public class ERTableDemo
{
    [ERTableName(tableStr(MyTable))]
    public static RecordLinkList getData(str _param) { ... }
}

In a model mapping, add a new Class data source and add your class. You’ll see a list of methods and if you expand the method, you’ll see the table fields available for binding.

When binding the method to a record list, we can provide a value for the parameter. We also can bind field values as usual:

But I would rather use an instance method on a table, which would produce data related to the given record (such as an invoice).

I saw instance methods with ERTableNameAttribute in standard code, therefore I knew it can be done on tables, but I wasn’t sure that ER takes table extensions into account.

I tried an extension like this:

[ExtensionOf(tableStr(CustInvoiceJour))]
public final class CustInvoiceJourMy_Extension
{
    [ERTableName(tableStr(MyTable))]
    public RecordLinkList getMyTableRecords()
    {
        ...
    }
}

and I am able to use it in ER model mapping:

This is ideal.

Getting attributes in X++

In X++, you can decorate classes and methods with attributes. Attributes were added in AX 2012 (I believe), where the typical use case was a definition of data contracts. They’re much more common in F&O, because they’re also used for CoC and event handlers.

For most developers, attributes are something defined by Microsoft and used by standard frameworks, but it’s something we can utilize for our own (advanced) scenarios when suitable.

You can easily define your own attributes (an attribute is simply a class inheriting from SysAttribute) and also check whether a class or method has a particular attribute and get attribute parameter values (e.g. the country from CountryRegionAttribute).

To get the attributes, you can use either Dict* classes or the new metadata API.

DictClass and DictMethod classes offer getAttribute() and getAttributes() to get attributes of a specific type, and getAllAttributes() to get all. For example:

DictClass dictClass = new DictClass(classNum(AssetAcquisitionGERContract));
Array attributes = dictClass.getAllAttributes();
 
for (int i = 1; i <= attributes.lastIndex(); i++)
{
    SysAttribute attribute = attributes.value(i);
    info(classId2Name(classIdGet(attribute)));
}

And here is the same thing with the metadata API:

using Microsoft.Dynamics.AX.Metadata.MetaModel;
...
AxClass axClass = Microsoft.Dynamics.Ax.Xpp.MetadataSupport::GetClass(classStr(AssetAcquisitionGERContract));
var enumerator = (axClass.Attributes as System.Collections.IList).GetEnumerator();
 
while (enumerator.MoveNext())
{
    AxAttribute attribute = enumerator.Current;
    info(attribute.Name);
}

Run settings for SysTest

When you execute automated tests of X++ code with SysTest, the test service (SysTestService class) gets called with some parameters, defined in SysTestRunnerSettings:

You could, for example, set granularity to execute just unit tests and skip integration tests, or produce a trace file for diagnostics.

You may want to use such parameters in automatic processes (e.g. running certain types of tests on gated builds) or directly in Visual Studio.

Visual Studio supports such configuration with .runsettings files (see Configure unit tests by using a .runsettings file). In Visual Studio, you can create a .runsettings file (which is a simple XML file), put it to your solution directory and it’ll be used automatically when running tests from Text Explorer. You can also select a particular file in Test > Configure Run Settings.

I wondered if the same approach can be used for SysTest parameters and it can indeed, you just need to put these parameters into SysTest element (and you must know the correct property names).

<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
  <SysTest>
      <RunInNewPartition>false</RunInNewPartition>
      <TraceFile>c:\temp\TestTrace.txt</TraceFile>
  </SysTest>
</RunSettings>