-
Notifications
You must be signed in to change notification settings - Fork 1k
@Observed #3221
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
@Observed #3221
Changes from all commits
ea8cae9
15f6eef
948fea0
b0500df
5dd62b2
c2d6b6a
90e8917
577e465
531d477
195c0b2
9fca396
25dde69
fbecfc1
8a3e715
ca42030
f0a664b
0c92b45
b344b15
2befb03
096a7af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 {}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why aren't we allowing to set Also, shouldn't we give an option to provide a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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.
It makes sense to me. I did write a PoC and also proposed a PR for a pretty similar feature for Check out the issue (there is also a PR), the PoC tries to do two things:
I think if we want to do something like this (I think it would be a nice feature) we should keep
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<ProceedingJoinPoint></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> | ||
* @Bean | ||
* public ObservedAspect observedAspect(ObservationRegistry observationRegistry) { | ||
* return new ObservedAspect(observationRegistry, this::skipControllers); | ||
* } | ||
* | ||
* private boolean skipControllers(ProceedingJoinPoint pjp) { | ||
* Class<?> targetClass = pjp.getTarget().getClass(); | ||
* return targetClass.isAnnotationPresent(RestController.class) || targetClass.isAnnotationPresent(Controller.class); | ||
* } | ||
* </pre> | ||
* | ||
* @author Jonatan Ivanov | ||
jonatan-ivanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @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; | ||
jonatan-ivanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 { | ||
jonatan-ivanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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; |
Uh oh!
There was an error while loading. Please reload this page.