NewRelic Logs With Logback on Azure Cloud

Those who work with NewRelic logs for Java applications probably know that the user of an agent might be the most flexible and convenient way to collect log data and gather different application metrics, like CPU and memory usage and execution time. However, to run a Java application with the agent, we need access to an application execution unit (like a jar or Docker image) and control an execution runtime (e.g., an image with JVM on Kubernetes). Having such access, we can set up Java command line arguments, including necessary configurations for the agent.

Though, Azure Cloud-managed services, such as Azure Data Factory (ADF) or Logic Apps, do not have "tangible" executed artifacts nor execution environments. Such services are fully managed, and their execution runtimes are virtual. For these services, Azure suggests using Insights and Monitor to collect logs, metrics, and events to create alerts. But if your company has a separate solution for centralized log collection, like NewRelic, it could be a challenge to integrate managed services with an external logging framework.

In this article, I will show how to send logs to NewRelic from Azure-managed services.

Solution

NewRelic provides a rather simple Log API, which imposes few restrictions and can be used by registered users authenticated by NewRelic API Key. Log API has only a mandatory message field and expects some fields in a predefined format, e.g., the timestamp should be as milliseconds or seconds since epoch or ISO8601-formatted string. A format of accepted messages is JSON.

Many Azure-managed services can send messages over HTTP. For instance, ADF provides an HTTP connector. Again, though, it might be troublesome to maintain consistent logging with the same log message structure, the same configurations, and the shared API key.

Instead, much more convenient would be to have a single service that is responsible for collecting messages from other Azure services and for communication with NewRelic. So, all the messages would go through the service, be transformed, and sent to NewRelic's Log API.

A good candidate for such a logging service is Azure Function with HTTP trigger. Azure Function can execute code to receive, validate, transform, and send log messages to NewRelic. And a function will be a single place where the NewRelic API key is used.

Logging from Azure Data Factory to NewRelic

Implementation

Depending on a log structure, a logger function may expect different message formats. Azure functions are able to process JSON payload and automatically deserialize it into an object. This feature helps to maintain a unified log format. The definition of a function's HTTP trigger would be like this:

Java
 
    public HttpResponseMessage log(
            @HttpTrigger(name = "req", 
                    methods = {HttpMethod.POST}, 
                    authLevel = AuthorizationLevel.ANONYMOUS) 
            HttpRequestMessage<Optional<LogEvent>> request,
            ExecutionContext context) {...}


Here LogEvent is POJO class, which is used to convert JSON strings into Java objects.

The reduction of implementation complexity can be achieved by splitting the functionality into two parts: a "low" level protocol component and the function itself that receives and logs input events. This separation of concerns, as the function does its main job - the logging, and a separate component implements NewRelic Log API. 

The logging-in function can be delegated to Logback or Log4j with Slf4j, so the process looks like conventional logging. No extra dependencies in the code except on a habitual Slf4j:

Java
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;

public class NewRelicLogger {

    private static final Logger LOG = 
      LoggerFactory.getLogger("newrelic-logger-function");
  
    @FunctionName("newrelic-logger")
    public HttpResponseMessage log(
            @HttpTrigger(name = "req",
                         methods = {HttpMethod.POST},
                         authLevel = AuthorizationLevel.ANONYMOUS)
            HttpRequestMessage<Optional<LogEvent>> request,
            ExecutionContext context) {

      // input parameters checks are skipped for simplicity
      
      var body = request.getBody().get();
      var level = body.level();
      var message = body.message();
      switch (level) {
        case "TRACE", "DEBUG", "INFO", "WARN", "ERROR" -> 
          LOG.atLevel(Level.valueOf(level)).log(message);
        default -> 
          LOG.info("Unknown level [{}]. Message: [{}]", level, message);
      }
      return request
        .createResponseBuilder(HttpStatus.OK)
        .body("")
        .build();
    }
}


The second part of the implementation is a custom appender which is used by Logback, so log messages are sent to NewRelic Log API. Logstash Logback Encoder is perfectly suited for the generation of JSON strings within the Logback appender. With it, we just need to implement an abstract AppenderBase. And LoggingEventCompositeJsonLayout will be responsible for the generation of JSON. The layout is highly configurable, which allows to form any structure of a message. A skeleton of AppenderBase implementation with mandatory fields can look like this:

Java
 
public class NewRelicAppender extends AppenderBase<ILoggingEvent> {
    private Layout<ILoggingEvent> layout;
    private String url;
    private String apiKey;
  
    // setters and getters are skipped

    @Override
    public void start() {
        if (!validateSettings()) { //check mandatory fields
            return;
        }
        super.start();
    }

    @Override
    protected void append(ILoggingEvent loggingEvent) {
      //layout generates JSON message
      var event = layout.doLayout(loggingEvent);
      
      // need to check the size of message
      // as NewRelic allows messages < 1Mb
      
      // NewRelic accepts messages compressed by GZip
      payload = compressIfNeeded(event);
      
      // send a message to NewRelic by some Http client
      sendPayload(payload);
    }
  
    // compression and message sending methods are skipped

}


The appender can be added, built, and packaged into the Azure function. Although, the better approach is to build the appender separately, so it can be reused in other projects without the necessity to copy-paste the code just by adding the appender as a dependency.

In the NewRelic Logback Appender repository, you can find an example of such an appender. And below, it will be shown how to use the appender with the logging function.

Usage

So, if the NewRelic appender is built as a separate artifact, it needs to be added as a dependency into a project. In our case, the project is the Azure logging function. The function uses a Logback framework for logging. Maven's dependencies will be like the following:

XML
 
    <dependencies>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.4.6</version>
        </dependency>
        <dependency>
            <groupId>ua.rapp</groupId>
            <artifactId>nr-logback-appender</artifactId>
            <version>0.1.0</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>


The appender uses LoggingEventCompositeJsonLayout. Its documentation can be found on Composite Encoder/Layout. The layout is configured with a mandatory field message and common attributes: timestamp, level, and logger. Custom attributes can be added into a resulting JSON via Slf4j MDC.

Other required appender's properties are needed to post log messages to NewRelic:

Property name Description
apiKey Valid API key to be sent as HTTP header. Register a NewRelic account or use an existing one.
host NewRelic address, like https://log-api.eu.newrelic.com for Europe.
See details Introduction to the Log API.
URL URL path to API, like /log/v1.

Here is a full example of the appender's configuration used by the NewRelic logger function:

XML
 
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <appender name="NEWRELIC" class="ua.rapp.logback.appenders.NewRelicAppender">
        <host>https://log-api.newrelic.com}</host>
        <url>/log/v1}</url>
        <apiKey>set-your-valid-api-key</apiKey>
        <layout class="net.logstash.logback.layout.LoggingEventCompositeJsonLayout">
            <providers>
                <timestamp/>
                <version/>
                <uuid/>
                <pattern>
                    <pattern>
                        {
                        "timestamp": "%date{ISO8601}",
                        "logger": "%logger",
                        "level": "%level",
                        "message": "%message"
                        }
                    </pattern>
                </pattern>
                <mdc/>
                <tags/>
            </providers>
        </layout>
    </appender>

    <root level="TRACE">
        <appender-ref ref="NEWRELIC"/>
    </root>
</configuration>


With Slf4j Logger and MDC the logger function logs message like this:

Java
 
private static final Logger LOG = LoggerFactory.getLogger("newrelic-logger-function");
// ...
MDC.put("invocationId", UUID.randomUUID().toString());
MDC.put("app-name", "console");
MDC.put("errorMessage", "Missed ID attribute!");
LOG.error("User [555] logged in failed.");


Which generates a JSON string:

JSON
 
{
  "@timestamp": "2023-04-22T14:09:07.2011033Z",
  "@version": "1",
  "app-name": "console",
  "errorMessage": "Missed ID attribute!",
  "invocationId": "0d16c981-d342-43f1-8b89-4e3e3fde8974",
  "level": "ERROR",
  "logger": "newrelic-logger-function",
  "message": "User [555] logged in failed.",
  "newrelic.source": "api.logs",
  "timestamp": 1682172547201,
  "uuid": "24212215-c075-4644-a122-15c075f6447f"
}


Here attributes @timestamp, @version, and uuid are added by Logstash Logback layout providers <timestamp/>, <version/>, and <uuid/>. Attributes app-name, invocationId, and errorMessage are added by <mdc/> the provider.

Conclusion

In this article, it was described how, without much effort, to create a custom Logback appender, which can be used by Azure function to send log messages into NewRelic. Azure function with HTTP trigger is used inside Azure Cloud. It provides API to handle logs from managed cloud services without NewRelic's agent.

 

 

 

 

Top