Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ea8cae9
Add basic ObservedAspect
jonatan-ivanov Apr 29, 2022
15f6eef
Merge branch 'main' into observed
jonatan-ivanov May 13, 2022
948fea0
formatting + license
jonatan-ivanov May 13, 2022
b0500df
Change package name of Observation TCK
jonatan-ivanov May 13, 2022
5dd62b2
Merge branch 'observation-tck-package' into observed
jonatan-ivanov May 13, 2022
c2d6b6a
Move to new Observation TCK package name
jonatan-ivanov May 13, 2022
90e8917
Merge branch 'main' into observed
jonatan-ivanov Jun 10, 2022
577e465
KeyValuesProvider shouldSkip Predicate, longTask support
jonatan-ivanov Jun 11, 2022
531d477
Remove default KeyValuesProvider
jonatan-ivanov Jun 11, 2022
195c0b2
Fix error messages in ObservationContextAssert
jonatan-ivanov Jun 14, 2022
9fca396
Add CompletionStage support to ObservedAspect
jonatan-ivanov Jun 14, 2022
25dde69
Add ability to process class-level annotation
jonatan-ivanov Jun 14, 2022
fbecfc1
Add ability to define contextualName and lowCardinalityKeyValues in t…
jonatan-ivanov Jun 15, 2022
8a3e715
Add javadoc
jonatan-ivanov Jun 15, 2022
ca42030
Merge branch 'main' into observed
jonatan-ivanov Jun 17, 2022
f0a664b
Fixing error message in TestObservationRegistryAssert
jonatan-ivanov Jun 18, 2022
0c92b45
highCardinalityKeyValue -> highCardinalityKeyValues
jonatan-ivanov Jun 18, 2022
b344b15
Use DocumentedObservation
jonatan-ivanov Jun 22, 2022
2befb03
Merge branch 'main' into observed
jonatan-ivanov Jun 22, 2022
096a7af
Remove unnecessary changes on the Observation
jonatan-ivanov Jun 22, 2022
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
5 changes: 5 additions & 0 deletions micrometer-observation/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ dependencies {
// HttpServlet KeyValueProvider
optionalApi 'javax.servlet:javax.servlet-api'

// Aspects
optionalApi 'org.aspectj:aspectjweaver'

// log monitoring
testImplementation 'ch.qos.logback:logback-classic'
testImplementation 'org.apache.logging.log4j:log4j-core'

testImplementation project(':micrometer-observation-test')

testImplementation 'org.springframework:spring-context'

// JUnit 5
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'com.tngtech.archunit:archunit-junit5'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2022 VMware, Inc.
*
* 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 io.micrometer.observation.annotation;

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;

import io.micrometer.observation.Observation;

/**
* Annotation to mark classes and methods that you want to observe.
*
* @author Jonatan Ivanov
* @since 1.10.0
*/
@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Observed {

/**
* Name of the {@link Observation}.
* @return name of the {@link Observation}
*/
String name() default "";

/**
* Contextual name of the {@link Observation}.
* @return contextual name of the {@link Observation}
*/
String contextualName() default "";

/**
* Low cardinality key values.
* @return an array of low cardinality key values.
*/
String[] lowCardinalityKeyValues() default {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't we allowing to set highCardinalityKeyValues ?

Also, shouldn't we give an option to provide a KeyValuesProvider class ? It would have to have a default constructor (when someone doesn't want to calculate the tags from the annotated method arguments) or (maybe) a constructor that takes in Object[] where the array would be mapped to methods arguments? Or even maybe also a Method argument? Does it make any sense? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't we allowing to set highCardinalityKeyValues ?

Because attribute values of annotations must be constants, you can't have dynamic values since you can't even call a method there, only literals and constants are allowed.

Does it make any sense?

It makes sense to me. I did write a PoC and also proposed a PR for a pretty similar feature for @Timed years ago 😄, see #1586

Check out the issue (there is also a PR), the PoC tries to do two things:

  1. Add an @ExtraTag annotation which is like @SpanTag in Sleuth and somewhat can cover the Object[]/Method/JointPont scenario that you mentioned above.
  2. An ExtraTagsPropagation class which is similar to Sleuth 2's ExtraFieldPropagation.

I think if we want to do something like this (I think it would be a nice feature) we should keep @Timed and @Observed in feature parity to a degree.
Also, I think we should discuss about two things in terms of @Observed:

  1. Setting keyvalues based on the jointpoint/method/params (an extra annotation can cover that, see above)
  2. Setting keyvalues based on the context

Can we move this to a separate issue/PR?


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2022 VMware, Inc.
*
* 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.
*/
@NonNullApi
@NonNullFields
package io.micrometer.observation.annotation;

import io.micrometer.common.lang.NonNullApi;
import io.micrometer.common.lang.NonNullFields;
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
/*
* Copyright 2022 VMware, Inc.
*
* 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 io.micrometer.observation.aop;

import io.micrometer.common.KeyValues;
import io.micrometer.common.docs.KeyName;
import io.micrometer.common.lang.NonNullApi;
import io.micrometer.common.lang.Nullable;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.annotation.Observed;
import io.micrometer.observation.docs.DocumentedObservation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.reflect.Method;
import java.util.concurrent.CompletionStage;
import java.util.function.Predicate;

import static io.micrometer.observation.aop.ObservedAspect.ObservedAspectObservation.ObservedAspectLowCardinalityKeyName.CLASS_NAME;
import static io.micrometer.observation.aop.ObservedAspect.ObservedAspectObservation.ObservedAspectLowCardinalityKeyName.METHOD_NAME;

/**
* <p>
* AspectJ aspect for intercepting types or methods annotated with
* {@link Observed @Observed}.<br>
* The aspect supports programmatic customizations through constructor-injectable custom
* logic.
* </p>
* <p>
* You might want to add {@link io.micrometer.common.KeyValue}s programmatically to the
* {@link Observation}.<br>
* In this case, the {@link Observation.KeyValuesProvider} can help. It receives a
* {@link ObservedAspectContext} that also contains the {@link ProceedingJoinPoint} and
* returns the {@link io.micrometer.common.KeyValue}s that will be attached to the
* {@link Observation}.
* </p>
* <p>
* You might also want to skip the {@link Observation} creation programmatically.<br>
* One use-case can be having another component in your application that already processes
* the {@link Observed @Observed} annotation in some cases so that {@code ObservedAspect}
* should not intercept these methods. E.g.: Spring Boot does this for its controllers. By
* using the skip predicate (<code>Predicate&lt;ProceedingJoinPoint&gt;</code>) you can
* tell the {@code ObservedAspect} when not to create a {@link Observation}.
*
* Here's an example to disable {@link Observation} creation for Spring controllers:
* </p>
* <pre>
* &#064;Bean
* public ObservedAspect observedAspect(ObservationRegistry observationRegistry) {
* return new ObservedAspect(observationRegistry, this::skipControllers);
* }
*
* private boolean skipControllers(ProceedingJoinPoint pjp) {
* Class&lt;?&gt; targetClass = pjp.getTarget().getClass();
* return targetClass.isAnnotationPresent(RestController.class) || targetClass.isAnnotationPresent(Controller.class);
* }
* </pre>
*
* @author Jonatan Ivanov
* @since 1.10.0
*/
@Aspect
@NonNullApi
public class ObservedAspect {

private static final String DEFAULT_OBSERVATION_NAME = "method.observed";

private static final Predicate<ProceedingJoinPoint> DONT_SKIP_ANYTHING = pjp -> false;

private final ObservationRegistry registry;

@Nullable
private final Observation.KeyValuesProvider<ObservedAspectContext> keyValuesProvider;

private final Predicate<ProceedingJoinPoint> shouldSkip;

public ObservedAspect(ObservationRegistry registry) {
this(registry, null, DONT_SKIP_ANYTHING);
}

public ObservedAspect(ObservationRegistry registry,
Observation.KeyValuesProvider<ObservedAspectContext> keyValuesProvider) {
this(registry, keyValuesProvider, DONT_SKIP_ANYTHING);
}

public ObservedAspect(ObservationRegistry registry, Predicate<ProceedingJoinPoint> shouldSkip) {
this(registry, null, shouldSkip);
}

public ObservedAspect(ObservationRegistry registry,
@Nullable Observation.KeyValuesProvider<ObservedAspectContext> keyValuesProvider,
Predicate<ProceedingJoinPoint> shouldSkip) {
this.registry = registry;
this.keyValuesProvider = keyValuesProvider;
this.shouldSkip = shouldSkip;
}

@Around("@within(io.micrometer.observation.annotation.Observed)")
@Nullable
public Object observeClass(ProceedingJoinPoint pjp) throws Throwable {
if (shouldSkip.test(pjp)) {
return pjp.proceed();
}

Method method = ((MethodSignature) pjp.getSignature()).getMethod();
Observed observed = getDeclaringClass(pjp).getAnnotation(Observed.class);
return observe(pjp, method, observed);
}

@Around("execution (@io.micrometer.observation.annotation.Observed * *.*(..))")
@Nullable
public Object observeMethod(ProceedingJoinPoint pjp) throws Throwable {
if (shouldSkip.test(pjp)) {
return pjp.proceed();
}

Method method = getMethod(pjp);
Observed observed = method.getAnnotation(Observed.class);
return observe(pjp, method, observed);
}

private Object observe(ProceedingJoinPoint pjp, Method method, Observed observed) throws Throwable {
Observation observation = ObservedAspectObservation.of(pjp, method, observed, this.registry,
this.keyValuesProvider);
if (CompletionStage.class.isAssignableFrom(method.getReturnType())) {
observation.start();
Observation.Scope scope = observation.openScope();
try {
return ((CompletionStage<?>) pjp.proceed())
.whenComplete((result, error) -> stopObservation(observation, scope, error));
}
catch (Throwable error) {
stopObservation(observation, scope, error);
throw error;
}
}
else {
return observation.observeChecked(() -> pjp.proceed());
}
}

private Class<?> getDeclaringClass(ProceedingJoinPoint pjp) {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
Class<?> declaringClass = method.getDeclaringClass();
if (!declaringClass.isAnnotationPresent(Observed.class)) {
return pjp.getTarget().getClass();
}

return declaringClass;
}

private Method getMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
if (method.getAnnotation(Observed.class) == null) {
return pjp.getTarget().getClass().getMethod(method.getName(), method.getParameterTypes());
}

return method;
}

private void stopObservation(Observation observation, Observation.Scope scope, @Nullable Throwable error) {
if (error != null) {
observation.error(error);
}
scope.close();
observation.stop();
}

public enum ObservedAspectObservation implements DocumentedObservation {

DEFAULT;

static Observation of(ProceedingJoinPoint pjp, Method method, Observed observed, ObservationRegistry registry,
@Nullable Observation.KeyValuesProvider<ObservedAspectContext> keyValuesProvider) {
String name = observed.name().isEmpty() ? DEFAULT_OBSERVATION_NAME : observed.name();
Signature signature = pjp.getStaticPart().getSignature();
String contextualName = observed.contextualName().isEmpty()
? signature.getDeclaringType().getSimpleName() + "#" + signature.getName()
: observed.contextualName();

Observation observation = Observation.createNotStarted(name, new ObservedAspectContext(pjp), registry)
.contextualName(contextualName)
.lowCardinalityKeyValue(CLASS_NAME.getKeyName(), signature.getDeclaringTypeName())
.lowCardinalityKeyValue(METHOD_NAME.getKeyName(), signature.getName())
.lowCardinalityKeyValues(KeyValues.of(observed.lowCardinalityKeyValues()));

if (keyValuesProvider != null) {
observation.keyValuesProvider(keyValuesProvider);
}

return observation;
}

@Override
public String getName() {
return "%s";
}

@Override
public String getContextualName() {
return "%s";
}

@Override
public KeyName[] getLowCardinalityKeyNames() {
return ObservedAspectLowCardinalityKeyName.values();
}

public enum ObservedAspectLowCardinalityKeyName implements KeyName {

CLASS_NAME {
@Override
public String getKeyName() {
return "class";
}
},

METHOD_NAME {
@Override
public String getKeyName() {
return "method";
}
}

}

}

public static class ObservedAspectContext extends Observation.Context {

private final ProceedingJoinPoint proceedingJoinPoint;

public ObservedAspectContext(ProceedingJoinPoint proceedingJoinPoint) {
this.proceedingJoinPoint = proceedingJoinPoint;
}

public ProceedingJoinPoint getProceedingJoinPoint() {
return this.proceedingJoinPoint;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2022 VMware, Inc.
*
* 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.
*/
@NonNullApi
@NonNullFields
package io.micrometer.observation.aop;

import io.micrometer.common.lang.NonNullApi;
import io.micrometer.common.lang.NonNullFields;
Loading