Skip to content

Commit

Permalink
feat: first functional draft log4jAppender
Browse files Browse the repository at this point in the history
  • Loading branch information
gsidhwani-nr committed Oct 14, 2024
1 parent b9a65b3 commit 384774c
Show file tree
Hide file tree
Showing 6 changed files with 432 additions and 0 deletions.
42 changes: 42 additions & 0 deletions custom-log4j-appender/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '6.1.0'
}

repositories {
mavenCentral() // Ensure you are using Maven Central repository
}

dependencies {
// OkHttp for HTTP requests
implementation 'com.squareup.okhttp3:okhttp:4.9.3'

// Jackson for JSON processing
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.1'

// Log4j Core for logging
implementation 'org.apache.logging.log4j:log4j-core:2.14.1'
implementation 'org.apache.logging.log4j:log4j-api:2.14.1'
}

jar {
manifest {
attributes(
'Implementation-Title': 'com.newrelic.labs.custom-log4j-appender',
'Implementation-Vendor': 'New Relic Labs',
'Implementation-Vendor-Id': 'com.newrelic.labs',
'Implementation-Version': '1.0'
)
}
}

shadowJar {
mergeServiceFiles()
manifest {
attributes 'Main-Class': 'com.newrelic.labs.logforwarder.MAIN'
}
}

tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.newrelic.labs;

public class LogEntry {
private final String message;
private final String applicationName;
private final String name;
private final String logtype;
private final long timestamp;

public LogEntry(String message, String applicationName, String name, String logtype, long timestamp) {
this.message = message;
this.applicationName = applicationName;
this.name = name;
this.logtype = logtype;
this.timestamp = timestamp;
}

public String getMessage() {
return message;
}

public String getApplicationName() {
return applicationName;
}

public String getName() {
return name;
}

public String getLogType() {
return logtype;
}

public long getTimestamp() {
return timestamp;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package com.newrelic.labs;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.zip.GZIPOutputStream;

import com.fasterxml.jackson.databind.ObjectMapper;

import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public class LogForwarder {
private final BlockingQueue<LogEntry> logQueue;
private final String apiKey;
private final String apiURL;
private final OkHttpClient client = new OkHttpClient();
private final ObjectMapper objectMapper = new ObjectMapper();
private final long maxMessageSize;

public LogForwarder(String apiKey, String apiURL, long maxMessageSize, BlockingQueue<LogEntry> logQueue) {
this.apiKey = apiKey;
this.apiURL = apiURL;
this.maxMessageSize = maxMessageSize;
this.logQueue = logQueue;
}

public void addToQueue(List<String> lines, String applicationName, String name, String logtype) {
for (String line : lines) {
logQueue.add(new LogEntry(line, applicationName, name, logtype, System.currentTimeMillis()));
}
}

public boolean isInitialized() {
return apiKey != null && apiURL != null;
}

public void flush(List<LogEntry> logEntries) {
InetAddress localhost = null;
try {
localhost = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}

String hostname = localhost != null ? localhost.getHostName() : "unknown";

@SuppressWarnings("unused")
MediaType mediaType = MediaType.parse("application/json");

try {
List<Map<String, Object>> logEvents = new ArrayList<>();
for (LogEntry entry : logEntries) {
Map<String, Object> logEvent = objectMapper.convertValue(entry, LowercaseKeyMap.class);
logEvent.put("hostname", hostname);
logEvent.put("logtype", entry.getLogType());
logEvent.put("timestamp", entry.getTimestamp()); // Use log creation timestamp
logEvent.put("applicationName", entry.getApplicationName());
logEvent.put("name", entry.getName());
logEvent.put("source", "NRBatchingAppender"); // Add custom field
logEvents.add(logEvent);
}

String jsonPayload = objectMapper.writeValueAsString(logEvents);
byte[] compressedPayload = gzipCompress(jsonPayload);

if (compressedPayload.length > maxMessageSize) { // Configurable size limit
System.err.println("Batch size exceeds limit, splitting batch...");
List<LogEntry> subBatch = new ArrayList<>();
int currentSize = 0;
for (LogEntry entry : logEntries) {
String entryJson = objectMapper.writeValueAsString(entry);
int entrySize = gzipCompress(entryJson).length;
if (currentSize + entrySize > maxMessageSize) {
sendLogs(subBatch);
subBatch.clear();
currentSize = 0;
}
subBatch.add(entry);
currentSize += entrySize;
}
if (!subBatch.isEmpty()) {
sendLogs(subBatch);
}
} else {
sendLogs(logEntries);
}
} catch (IOException e) {
System.err.println("Error during log forwarding: " + e.getMessage());
}
}

private void sendLogs(List<LogEntry> logEntries) throws IOException {
InetAddress localhost = InetAddress.getLocalHost();
String hostname = localhost.getHostName();

List<Map<String, Object>> logEvents = new ArrayList<>();
for (LogEntry entry : logEntries) {
Map<String, Object> logEvent = objectMapper.convertValue(entry, LowercaseKeyMap.class);
logEvent.put("hostname", hostname);
logEvent.put("logtype", entry.getLogType());
logEvent.put("applicationName", entry.getApplicationName());
logEvent.put("name", entry.getName());
logEvent.put("timestamp", entry.getTimestamp()); // Use log creation timestamp
logEvent.put("source", "NRBatchingAppender"); // Add custom field
logEvents.add(logEvent);
}

String jsonPayload = objectMapper.writeValueAsString(logEvents);
byte[] compressedPayload = gzipCompress(jsonPayload);

MediaType mediaType = MediaType.parse("application/json");

RequestBody requestBody = RequestBody.create(compressedPayload, mediaType);
Request request = new Request.Builder().url(apiURL).post(requestBody).addHeader("X-License-Key", apiKey)
.addHeader("Content-Type", "application/json").addHeader("Content-Encoding", "gzip").build();

try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
System.err.println("Failed to send logs to New Relic: " + response.code() + " - " + response.message());
System.err.println("Response body: " + response.body().string());
} else {
LocalDateTime timestamp = LocalDateTime.now();
System.out.println("Logs sent to New Relic successfully: " + "at " + timestamp + " size: "
+ compressedPayload.length + " Bytes");
System.out.println("Response: " + response.body().string());
}
}
}

private byte[] gzipCompress(String input) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (GZIPOutputStream gzipOS = new GZIPOutputStream(bos)) {
gzipOS.write(input.getBytes());
}
return bos.toByteArray();
}

public void close() {
List<LogEntry> remainingLogs = new ArrayList<>();
logQueue.drainTo(remainingLogs);
if (!remainingLogs.isEmpty()) {
System.out.println("Flushing remaining " + remainingLogs.size() + " log events to New Relic...");
flush(remainingLogs);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.newrelic.labs;

import java.util.HashMap;
import java.util.Map;

@SuppressWarnings("serial")
public class LowercaseKeyMap extends HashMap<String, Object> {
@Override
public Object put(String key, Object value) {
return super.put(key.toLowerCase(), value);
}

@Override
public void putAll(Map<? extends String, ? extends Object> m) {
for (Map.Entry<? extends String, ? extends Object> entry : m.entrySet()) {
this.put(entry.getKey().toLowerCase(), entry.getValue());
}
}
}
Loading

0 comments on commit 384774c

Please sign in to comment.