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

Observation text publisher #3034

Merged
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
Add ObservationTextPublisher
So that it is easy to publish the text representation of the context to somewhere (e.g.: stdout)
  • Loading branch information
jonatan-ivanov committed Feb 19, 2022
commit 08531976f42ef61f1ad375e27a46100a1145bc41
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.micrometer.api.instrument.observation;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
Expand All @@ -24,6 +25,7 @@
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.micrometer.api.instrument.Tag;
Expand Down Expand Up @@ -302,19 +304,6 @@ class Context {

private final Set<Tag> highCardinalityTags = new LinkedHashSet<>();

/**
* Puts an element to the context.
*
* @param key key
* @param object value
* @param <T> value type
* @return this for chaining
*/
public <T> Context put(Object key, T object) {
this.map.put(key, object);
return this;
}

/**
* The observation name.
*
Expand Down Expand Up @@ -377,12 +366,16 @@ public Context setError(Throwable error) {
}

/**
* Removes an entry from the context.
* Puts an element to the context.
*
* @param key key by which to remove an entry
* @param key key
* @param object value
* @param <T> value type
* @return this for chaining
*/
public void remove(Object key) {
this.map.remove(key);
public <T> Context put(Object key, T object) {
this.map.put(key, object);
return this;
}

/**
Expand All @@ -397,6 +390,15 @@ public <T> T get(Object key) {
return (T) this.map.get(key);
}

/**
* Removes an entry from the context.
*
* @param key key by which to remove an entry
*/
public void remove(Object key) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should return the removed object?

Copy link
Member Author

Choose a reason for hiding this comment

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

👍🏼 I just moved this method, did not change the signature but we should definitely do this.

this.map.remove(key);
}

/**
* Gets an entry from the context. Throws exception when entry is not present.
*
Expand Down Expand Up @@ -510,13 +512,24 @@ public Tags getAllTags() {

@Override
public String toString() {
return "Context{" +
"map=" + map +
", name='" + name + '\'' +
return "name='" + name + '\'' +
", contextualName='" + contextualName + '\'' +
", lowCardinalityTags=" + getLowCardinalityTags() +
", highCardinalityTags=" + getHighCardinalityTags() +
'}';
", error='" + error + '\'' +
", lowCardinalityTags=" + toString(lowCardinalityTags) +
", highCardinalityTags=" + toString(highCardinalityTags) +
", map=" + toString(map);
}

private String toString(Collection<Tag> tags) {
return tags.stream()
.map(tag -> String.format("%s='%s'", tag.getKey(), tag.getValue()))
.collect(Collectors.joining(", ", "[", "]"));
}

private String toString(Map<Object, Object> map) {
return map.entrySet().stream()
.map(entry -> String.format("%s='%s'", entry.getKey(), entry.getValue()))
.collect(Collectors.joining(", ", "[", "]"));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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.api.instrument.observation;

import java.util.function.Consumer;
import java.util.function.Predicate;

/**
* An {@link ObservationHandler} that converts the context to text and Publishes it to the {@link Consumer} of your choice.
*
* @author Jonatan Ivanov
* @since 2.0.0
*/
public class ObservationTextPublisher implements ObservationHandler<Observation.Context> {
private final Consumer<String> consumer;
private final Predicate<Observation.Context> supportsContextPredicate;

/**
* Creates a publisher that sends the context as text to the given {@link Consumer}.
*
* @param consumer Where to publish the context as text
*/
public ObservationTextPublisher(Consumer<String> consumer) {
this(consumer, context -> true);
}

/**
* Creates a publisher that sends the context as text to the given {@link Consumer} if the {@link Predicate} returns true.
*
* @param consumer Where to publish the context as text
* @param supportsContextPredicate Whether the publisher should support the given context
*/
public ObservationTextPublisher(Consumer<String> consumer, Predicate<Observation.Context> supportsContextPredicate) {
this.consumer = consumer;
this.supportsContextPredicate = supportsContextPredicate;
}

@Override
public void onStart(Observation.Context context) {
publish("START", context);
}

@Override
public void onError(Observation.Context context) {
publish("ERROR", context);
}

@Override
public void onScopeOpened(Observation.Context context) {
publish("OPEN", context);
}

@Override
public void onScopeClosed(Observation.Context context) {
publish("CLOSE", context);
}

@Override
public void onStop(Observation.Context context) {
publish("STOP", context);
}

@Override
public boolean supportsContext(Observation.Context context) {
return this.supportsContextPredicate.test(context);
}

private void publish(String event, Observation.Context context) {
this.consumer.accept(String.format("%5s - %s", event, context));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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.api.instrument.observation;

import java.io.IOException;
import java.util.function.Consumer;

import io.micrometer.api.instrument.Tag;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for {@link ObservationTextPublisher}.
*
* @author Jonatan Ivanov
*/
class ObservationTextPublisherTests {
private static final String CONTEXT_TOSTRING = "name='testName', contextualName='testContextualName', error='java.io.IOException: simulated', lowCardinalityTags=[lcTag='foo'], highCardinalityTags=[hcTag='bar'], map=[contextKey='contextValue']";
private final TestConsumer consumer = new TestConsumer();
private final ObservationHandler<Observation.Context> publisher = new ObservationTextPublisher(consumer);

@Test
void onStartShouldPublishStartEvent() {
publisher.onStart(createTestContext());
assertThat(consumer.toString()).isEqualTo("START - " + CONTEXT_TOSTRING);
}

@Test
void onScopeOpenedShouldPublishOpenEvent() {
publisher.onScopeOpened(createTestContext());
assertThat(consumer.toString()).isEqualTo("OPEN - " + CONTEXT_TOSTRING);
}

@Test
void onErrorShouldPublishErrorEvent() {
publisher.onError(createTestContext());
assertThat(consumer.toString()).isEqualTo("ERROR - " + CONTEXT_TOSTRING);
}

@Test
void onScopeClosedShouldPublishCloseEvent() {
publisher.onScopeClosed(createTestContext());
assertThat(consumer.toString()).isEqualTo("CLOSE - " + CONTEXT_TOSTRING);
}

@Test
void onStopClosedShouldPublishCloseEvent() {
publisher.onStop(createTestContext());
assertThat(consumer.toString()).isEqualTo("STOP - " + CONTEXT_TOSTRING);
}

@Test
void shouldSupportAnyContextByDefault() {
assertThat(publisher.supportsContext(null)).isTrue();
assertThat(publisher.supportsContext(new Observation.Context())).isTrue();
assertThat(publisher.supportsContext(createTestContext())).isTrue();
}

@Test
void shouldSupportContextEnabledByThePredicate() {
ObservationHandler<Observation.Context> publisher = new ObservationTextPublisher(consumer, context -> "testName".equals(context.getName()));
assertThat(publisher.supportsContext(new Observation.Context())).isFalse();
assertThat(publisher.supportsContext(createTestContext())).isTrue();
}

private Observation.Context createTestContext() {
Observation.Context context = new Observation.Context()
.setName("testName")
.setContextualName("testContextualName")
.setError(new IOException("simulated"));
context.addLowCardinalityTag(Tag.of("lcTag", "foo"));
context.addHighCardinalityTag(Tag.of("hcTag", "bar"));
context.put("contextKey", "contextValue");

return context;
}

static class TestConsumer implements Consumer<String> {
private final StringBuilder stringBuilder = new StringBuilder();

@Override
public void accept(String text) {
stringBuilder.append(text).append("\n");
}

@Override
public String toString() {
return stringBuilder.toString().trim();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,14 @@ void observationFieldsShouldBeSetOnContext() {

assertThat(context.getContextualName()).isEqualTo("test.observation.42");
assertThat(context.getError()).containsSame(exception);

assertThat(context.toString())
.containsOnlyOnce("map={context.field=42}")
.containsOnlyOnce("name='test.observation'")
.containsOnlyOnce("contextualName='test.observation.42'")
.containsOnlyOnce("lowCardinalityTags=[tag(global.context.class=TestContext),tag(lcTag1=1),tag(lcTag2=2),tag(local.context.class=TestContext)]")
.containsOnlyOnce("highCardinalityTags=[tag(global.uuid=" + testContext.uuid + "),tag(hcTag1=3),tag(hcTag2=4),tag(local.uuid=" + testContext.uuid + ")]");
.containsOnlyOnce("error='java.io.IOException: simulated'")
.containsOnlyOnce("lowCardinalityTags=[lcTag1='1', lcTag2='2', global.context.class='TestContext', local.context.class='TestContext']")
.containsOnlyOnce("highCardinalityTags=[hcTag1='3', hcTag2='4', global.uuid='" + testContext.uuid + "', local.uuid='" + testContext.uuid + "']")
.containsOnlyOnce("map=[context.field='42']");
});
}

Expand Down
Loading