Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Critical events #32

Merged
merged 5 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/guide/src/docs/asciidoc/usage.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ WARNING: If you use the library in a non-Micronaut environment (e.g., Grails), e

The events can optionally implement the `NewRelicInsightsEvent` interface,
which let you fine-tune the `eventType` and `timestamp` properties.

There might be some HTTP communication issues when communicating with the NewRelic Insights API when using the HTTP client. By default, the error is only logged as a warning. You can fine tune the logging level by setting `newrelic.log-level` Micronaut property to one of the possible values: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF`.

You can mark your events with `@Critical` annotation to try to retry the event delivery when there is any communication issue such as connection timeout or reset. The default number of retries is 3, but you can change it by setting the `newrelic.retry-count` Micronaut property. If you are using `NewRelicInsightsEvent` interface, you can also mark your event critical by returning `true` from the `isCritical` method. If you are not implementing the `NewRelicInsightsEvent` interface then you can still add `critical` property to your event and set it to `true`.
1 change: 1 addition & 0 deletions libs/micronaut-newrelic/micronaut-newrelic.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies {
implementation 'io.micronaut.validation:micronaut-validation'
implementation "io.projectreactor:reactor-core:$projectReactorVersion"
implementation 'io.micronaut:micronaut-jackson-databind'
implementation 'io.micronaut:micronaut-retry'

// We don't have a runtime dependency on NR to be able to use the plain HTTP synchronous client on Lambdas.
compileOnly 'io.micronaut:micronaut-http-client'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public AsyncNewRelicInsightsService(Insights insights, EventPayloadExtractor ext
this.extractor = extractor;
}

@Override
public <E> void createEvents(Collection<E> events) {
unsafeCreateEvents(events);
}

@Override
public <E> void unsafeCreateEvent(@NonNull @Valid E event) {
Map<String, Object> map = extractor.extractPayload(event);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public <E> Map<String, Object> extractPayload(E event) {

map.computeIfAbsent("eventType", k -> introspection.getBeanType().getSimpleName());
map.computeIfAbsent("timestamp", k -> System.currentTimeMillis());
map.computeIfAbsent("critical", k -> introspection.findAnnotation(Critical.class).isPresent());
return map;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2022-2024 Agorapulse.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.agorapulse.micronaut.newrelic;

import io.micronaut.core.annotation.Introspected;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Critical events are retried when sending to New Relic and there are some troubles sending the events (e.g. connection timeouts and resets).
*/
@Inherited
@Documented
@Introspected
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Critical {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2022-2024 Agorapulse.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.agorapulse.micronaut.newrelic;

import io.micronaut.context.annotation.Requires;
import io.micronaut.retry.annotation.Retryable;
import jakarta.inject.Singleton;

import java.util.Map;

/**
* This client is used to send critical events to New Relic Insights.
*
* This class does not implement the {@link NewRelicInsightsClient} interface to avoid cyclic dependencies.
*/
@Singleton
@Requires(bean = NewRelicInsightsClient.class)
public class CriticalNewRelicInsightsClient {

private final NewRelicInsightsClient client;

public CriticalNewRelicInsightsClient(NewRelicInsightsClient client) {
this.client = client;
}

@Retryable(attempts = "${newrelic.retry.count:3}", predicate = NewRelicRetryPredicate.class)
public void createEvents(Iterable<Map<String, Object>> events) {
client.createEvents(events);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,9 @@ public Long getValue() {
return value;
}

@Override
public boolean isCritical() {
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@

import io.micronaut.core.annotation.NonNull;
import jakarta.inject.Singleton;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.stream.Collectors;
import java.util.List;
import java.util.Map;

/**
* Default NewRelicInsightsService, sends events to the New Relic API in real time, with a blocking request.
Expand All @@ -34,23 +39,91 @@
*/
@Primary
@Singleton
@Requires(
beans = NewRelicInsightsClient.class
)
@Requires(beans = NewRelicInsightsClient.class)
@Replaces(FallbackNewRelicInsightsService.class)
public class DefaultNewRelicInsightsService implements NewRelicInsightsService {

private static final Logger LOGGER = LoggerFactory.getLogger(DefaultNewRelicInsightsService.class);
private static final String DEFAULT_ERROR_MESSAGE = "Exception creating New Relic events ";

private final CriticalNewRelicInsightsClient criticalClient;
private final NewRelicInsightsClient client;
private final EventPayloadExtractor extractor;
private final NewRelicConfiguration configuration;

public DefaultNewRelicInsightsService(NewRelicInsightsClient client, EventPayloadExtractor extractor) {
public DefaultNewRelicInsightsService(
CriticalNewRelicInsightsClient criticalClient,
NewRelicInsightsClient client,
EventPayloadExtractor extractor,
NewRelicConfiguration configuration
) {
this.criticalClient = criticalClient;
this.client = client;
this.extractor = extractor;
this.configuration = configuration;
}

public <E> void createEvents(@Valid @NonNull Collection<E> events) {
try {
unsafeCreateEvents(events);
} catch (ConstraintViolationException cve) {
// keep the validation exceptions
throw cve;
} catch (Exception ex) {
boolean hasCriticalEvents = events.stream()
.map(extractor::extractPayload)
.anyMatch(EventPayloadExtractor::isCritical);

if (hasCriticalEvents) {
log(ex);
} else {
// only log events that won't trigger retry with critical client
if (!NewRelicRetryPredicate.INSTANCE.test(ex)) {
log(ex);
}
}
}
}

@Override
public <E> void unsafeCreateEvents(@NonNull @Valid Collection<E> events) {
this.client.createEvents(events.stream().map(extractor::extractPayload).collect(Collectors.toList()));
List<Map<String, Object>> criticalEvents = events.stream()
.map(extractor::extractPayload)
.filter(EventPayloadExtractor::isCritical)
.toList();

List<Map<String, Object>> nonCriticalEvents = events.stream()
.map(extractor::extractPayload)
.filter(EventPayloadExtractor::isNonCritical)
.toList();

if (!criticalEvents.isEmpty()) {
this.criticalClient.createEvents(criticalEvents);
}

if (!nonCriticalEvents.isEmpty()) {
this.client.createEvents(nonCriticalEvents);
}
}

private void log(Exception ex) {
switch (configuration.getLogLevel()) {
case TRACE:
LOGGER.trace(DEFAULT_ERROR_MESSAGE, ex);
break;
case DEBUG:
LOGGER.debug(DEFAULT_ERROR_MESSAGE, ex);
break;
case INFO:
LOGGER.info(DEFAULT_ERROR_MESSAGE, ex);
break;
case ERROR:
LOGGER.error(DEFAULT_ERROR_MESSAGE, ex);
break;
case WARN:
default:
LOGGER.warn(DEFAULT_ERROR_MESSAGE, ex);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@

public interface EventPayloadExtractor {

static boolean isCritical(Map<String, Object> payload) {
return payload.containsKey("critical") && Boolean.TRUE.equals(payload.get("critical"));
}

static boolean isNonCritical(Map<String, Object> payload) {
return !isCritical(payload);
}

/**
* Extracts the paylaod for the event.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
package com.agorapulse.micronaut.newrelic;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micronaut.context.annotation.Secondary;
import org.slf4j.Logger;
Expand All @@ -41,7 +42,15 @@ public FallbackNewRelicInsightsService(ObjectMapper mapper) {

@Override
public <E> void unsafeCreateEvents(@NonNull @Valid Collection<E> events) throws Exception {
LOGGER.info("Following events not sent to NewRelic:\n" + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(events));
createEvents(events);
}

@Override
public <E> void createEvents(Collection<E> events) {
try {
LOGGER.info("Following events not sent to NewRelic:\n{}", mapper.writerWithDefaultPrettyPrinter().writeValueAsString(events));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@
import io.micronaut.context.annotation.ConfigurationProperties;

import io.micronaut.core.annotation.Nullable;
import org.slf4j.event.Level;

@ConfigurationProperties("newrelic")
public class NewRelicConfiguration {

@Nullable private String url;
@Nullable private String token;

private Level logLevel = Level.WARN;

@Nullable public String getUrl() {
return url;
}
Expand All @@ -43,4 +46,11 @@ public void setToken(@Nullable String token) {
this.token = token;
}

public Level getLogLevel() {
return logLevel;
}

public void setLogLevel(Level logLevel) {
this.logLevel = logLevel;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import io.micronaut.core.annotation.Introspected;

import io.micronaut.core.beans.BeanIntrospector;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

Expand Down Expand Up @@ -47,4 +48,8 @@ default Long getTimestamp() {
return System.currentTimeMillis();
}

default boolean isCritical() {
return BeanIntrospector.SHARED.findIntrospection(getClass()).map(i -> i.findAnnotation(Critical.class).isPresent()).orElse(false);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@
package com.agorapulse.micronaut.newrelic;

import io.micronaut.validation.Validated;
import org.slf4j.LoggerFactory;

import io.micronaut.core.annotation.NonNull;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.Collections;
Expand All @@ -33,16 +31,7 @@ default <E> void createEvent(@Valid @NonNull E event) {
createEvents(Collections.singleton(event));
}

default <E> void createEvents(@Valid @NonNull Collection<E> events) {
try {
unsafeCreateEvents(events);
} catch (ConstraintViolationException cve) {
// keep the validation exceptions
throw cve;
} catch (Exception ex) {
LoggerFactory.getLogger(getClass()).error("Exception creating New Relic events " + ex);
}
}
<E> void createEvents(@Valid @NonNull Collection<E> events);

default <E> void unsafeCreateEvent(@Valid @NonNull E event) throws Exception {
unsafeCreateEvents(Collections.singleton(event));
Expand Down
Loading
Loading