diff --git a/core/pom.xml b/core/pom.xml
index f32245e5f..de3bb11cc 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -86,7 +86,6 @@
org.apache.maven.plugins
maven-enforcer-plugin
- 3.1.0
enforce-banned-dependencies
diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java
index a974e467c..31ae5b8d4 100644
--- a/core/src/main/java/feign/Request.java
+++ b/core/src/main/java/feign/Request.java
@@ -13,16 +13,17 @@
*/
package feign;
+import static feign.Util.checkNotNull;
+import static feign.Util.valuesOrEmpty;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.nio.charset.Charset;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
-import static feign.Util.checkNotNull;
-import static feign.Util.valuesOrEmpty;
/**
* An immutable request to an http server.
@@ -206,6 +207,26 @@ public Map> headers() {
return Collections.unmodifiableMap(headers);
}
+ /**
+ * Add new entries to request Headers. It overrides existing entries
+ *
+ * @param key
+ * @param value
+ */
+ public void header(String key, String value) {
+ header(key, Arrays.asList(value));
+ }
+
+ /**
+ * Add new entries to request Headers. It overrides existing entries
+ *
+ * @param key
+ * @param values
+ */
+ public void header(String key, Collection values) {
+ headers.put(key, values);
+ }
+
/**
* Charset of the request.
*
diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java
index 9014430fd..20bbb8b66 100644
--- a/core/src/main/java/feign/RequestTemplate.java
+++ b/core/src/main/java/feign/RequestTemplate.java
@@ -13,18 +13,32 @@
*/
package feign;
+import static feign.Util.CONTENT_LENGTH;
+import static feign.Util.checkNotNull;
import feign.Request.HttpMethod;
-import feign.template.*;
+import feign.template.BodyTemplate;
+import feign.template.HeaderTemplate;
+import feign.template.QueryTemplate;
+import feign.template.UriTemplate;
+import feign.template.UriUtils;
import java.io.Serializable;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.AbstractMap.SimpleImmutableEntry;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
import java.util.Map.Entry;
+import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
-import static feign.Util.*;
/**
* Request Builder for an HTTP Target.
@@ -764,12 +778,10 @@ private RequestTemplate appendHeader(String name, Iterable values, boole
} else {
return HeaderTemplate.create(headerName, values);
}
+ } else if (literal) {
+ return HeaderTemplate.appendLiteral(headerTemplate, values);
} else {
- if (literal) {
- return HeaderTemplate.appendLiteral(headerTemplate, values);
- } else {
- return HeaderTemplate.append(headerTemplate, values);
- }
+ return HeaderTemplate.append(headerTemplate, values);
}
});
return this;
@@ -791,7 +803,7 @@ public RequestTemplate headers(Map> headers) {
}
/**
- * Returns an immutable copy of the Headers for this request.
+ * Returns an copy of the Headers for this request.
*
* @return the currently applied headers.
*/
@@ -802,10 +814,10 @@ public Map> headers() {
/* add the expanded collection, but only if it has values */
if (!values.isEmpty()) {
- headerMap.put(key, Collections.unmodifiableList(values));
+ headerMap.put(key, values);
}
});
- return Collections.unmodifiableMap(headerMap);
+ return headerMap;
}
/**
diff --git a/core/src/test/java/feign/RequestTemplateTest.java b/core/src/test/java/feign/RequestTemplateTest.java
index dbfccdd3a..1becf347b 100644
--- a/core/src/test/java/feign/RequestTemplateTest.java
+++ b/core/src/test/java/feign/RequestTemplateTest.java
@@ -15,10 +15,13 @@
import static feign.assertj.FeignAssertions.assertThat;
import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.MapEntry.entry;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import feign.Request.HttpMethod;
+import feign.template.UriUtils;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -27,8 +30,6 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
-import feign.Request.HttpMethod;
-import feign.template.UriUtils;
public class RequestTemplateTest {
@@ -472,16 +473,25 @@ public void shouldRetrieveHeadersWithoutNull() {
}
- @SuppressWarnings("ConstantConditions")
- @Test(expected = UnsupportedOperationException.class)
- public void shouldNotInsertHeadersImmutableMap() {
+ public void shouldNotMutateInternalHeadersMap() {
RequestTemplate template = new RequestTemplate()
.header("key1", "valid");
assertThat(template.headers()).hasSize(1);
assertThat(template.headers().keySet()).containsExactly("key1");
+ assertThat(template.headers().get("key1")).containsExactly("valid");
template.headers().put("key2", Collections.singletonList("other value"));
+ // nothing should change
+ assertThat(template.headers()).hasSize(1);
+ assertThat(template.headers().keySet()).containsExactly("key1");
+ assertThat(template.headers().get("key1")).containsExactly("valid");
+
+ template.headers().get("key1").add("value2");
+ // nothing should change either
+ assertThat(template.headers()).hasSize(1);
+ assertThat(template.headers().keySet()).containsExactly("key1");
+ assertThat(template.headers().get("key1")).containsExactly("valid");
}
@Test
diff --git a/micrometer/pom.xml b/micrometer/pom.xml
index a2e670b10..66aceda74 100644
--- a/micrometer/pom.xml
+++ b/micrometer/pom.xml
@@ -27,6 +27,7 @@
${project.basedir}/..
+ 1.10.0
@@ -42,7 +43,7 @@
io.micrometer
micrometer-core
- 1.10.0
+ ${micrometer.version}
org.mockito
@@ -50,11 +51,23 @@
${mockito.version}
test
+
+ io.micrometer
+ micrometer-test
+ ${micrometer.version}
+ test
+
org.hamcrest
hamcrest
test
+
+ ${project.groupId}
+ feign-okhttp
+ ${project.version}
+ test
+
@@ -71,5 +84,4 @@
-
diff --git a/micrometer/src/main/java/feign/micrometer/DefaultFeignObservationConvention.java b/micrometer/src/main/java/feign/micrometer/DefaultFeignObservationConvention.java
new file mode 100644
index 000000000..ac000c8dc
--- /dev/null
+++ b/micrometer/src/main/java/feign/micrometer/DefaultFeignObservationConvention.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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
+ *
+ * http://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 feign.micrometer;
+
+import feign.Request;
+import feign.Response;
+import io.micrometer.common.KeyValues;
+import io.micrometer.common.lang.Nullable;
+
+/**
+ * Default implementation of {@link FeignObservationConvention}.
+ *
+ * @since 12.1
+ * @see FeignObservationConvention
+ */
+public class DefaultFeignObservationConvention implements FeignObservationConvention {
+
+ /**
+ * Singleton instance of this convention.
+ */
+ public static final DefaultFeignObservationConvention INSTANCE =
+ new DefaultFeignObservationConvention();
+
+ // There is no need to instantiate this class multiple times, but it may be extended,
+ // hence protected visibility.
+ protected DefaultFeignObservationConvention() {}
+
+ @Override
+ public String getName() {
+ return "http.client.requests";
+ }
+
+ @Override
+ public String getContextualName(FeignContext context) {
+ return "HTTP " + getMethodString(context.getCarrier());
+ }
+
+ @Override
+ public KeyValues getLowCardinalityKeyValues(FeignContext context) {
+ String templatedUrl = context.getCarrier().requestTemplate().methodMetadata().template().url();
+ return KeyValues.of(
+ FeignObservationDocumentation.HttpClientTags.METHOD
+ .withValue(getMethodString(context.getCarrier())),
+ FeignObservationDocumentation.HttpClientTags.URI
+ .withValue(templatedUrl),
+ FeignObservationDocumentation.HttpClientTags.STATUS
+ .withValue(getStatusValue(context.getResponse())));
+ }
+
+ String getStatusValue(@Nullable Response response) {
+ return response != null ? String.valueOf(response.status()) : "CLIENT_ERROR";
+ }
+
+ String getMethodString(@Nullable Request request) {
+ if (request == null) {
+ return "UNKNOWN";
+ }
+ return request.httpMethod().name();
+ }
+
+}
diff --git a/micrometer/src/main/java/feign/micrometer/FeignContext.java b/micrometer/src/main/java/feign/micrometer/FeignContext.java
new file mode 100644
index 000000000..1b1b3f9ab
--- /dev/null
+++ b/micrometer/src/main/java/feign/micrometer/FeignContext.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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
+ *
+ * http://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 feign.micrometer;
+
+import feign.Request;
+import feign.Response;
+import io.micrometer.observation.transport.RequestReplySenderContext;
+import io.micrometer.observation.transport.SenderContext;
+
+/**
+ * A {@link SenderContext} for Feign.
+ *
+ * @author Marcin Grzejszczak
+ * @since 12.1
+ */
+public class FeignContext extends RequestReplySenderContext {
+
+ public FeignContext(Request request) {
+ super(Request::header);
+ setCarrier(request);
+ }
+
+}
diff --git a/micrometer/src/main/java/feign/micrometer/FeignObservationConvention.java b/micrometer/src/main/java/feign/micrometer/FeignObservationConvention.java
new file mode 100644
index 000000000..1618e0dfb
--- /dev/null
+++ b/micrometer/src/main/java/feign/micrometer/FeignObservationConvention.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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
+ *
+ * http://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 feign.micrometer;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
+
+/**
+ * {@link ObservationConvention} for Feign.
+ *
+ * @since 12.1
+ */
+public interface FeignObservationConvention extends ObservationConvention {
+
+ @Override
+ default boolean supportsContext(Observation.Context context) {
+ return context instanceof FeignContext;
+ }
+
+}
diff --git a/micrometer/src/main/java/feign/micrometer/FeignObservationDocumentation.java b/micrometer/src/main/java/feign/micrometer/FeignObservationDocumentation.java
new file mode 100644
index 000000000..74f1b861e
--- /dev/null
+++ b/micrometer/src/main/java/feign/micrometer/FeignObservationDocumentation.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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
+ *
+ * http://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 feign.micrometer;
+
+import io.micrometer.common.docs.KeyName;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
+import io.micrometer.observation.docs.ObservationDocumentation;
+
+/**
+ * {@link ObservationDocumentation} for Feign.
+ *
+ * @since 12.1
+ */
+public enum FeignObservationDocumentation implements ObservationDocumentation {
+
+ DEFAULT {
+ @Override
+ public Class extends ObservationConvention extends Observation.Context>> getDefaultConvention() {
+ return DefaultFeignObservationConvention.class;
+ }
+
+ @Override
+ public KeyName[] getLowCardinalityKeyNames() {
+ return HttpClientTags.values();
+ }
+ };
+
+ enum HttpClientTags implements KeyName {
+
+ STATUS {
+ @Override
+ public String asString() {
+ return "http.status_code";
+ }
+ },
+ METHOD {
+ @Override
+ public String asString() {
+ return "http.method";
+ }
+ },
+ URI {
+ @Override
+ public String asString() {
+ return "http.url";
+ }
+ },
+ TARGET_SCHEME {
+ @Override
+ public String asString() {
+ return "http.scheme";
+ }
+ },
+ TARGET_HOST {
+ @Override
+ public String asString() {
+ return "net.peer.host";
+ }
+ },
+ TARGET_PORT {
+ @Override
+ public String asString() {
+ return "net.peer.port";
+ }
+ }
+
+ }
+
+}
diff --git a/micrometer/src/main/java/feign/micrometer/MicrometerObservationCapability.java b/micrometer/src/main/java/feign/micrometer/MicrometerObservationCapability.java
new file mode 100644
index 000000000..927691597
--- /dev/null
+++ b/micrometer/src/main/java/feign/micrometer/MicrometerObservationCapability.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2012-2022 The Feign Authors
+ *
+ * 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
+ *
+ * http://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 feign.micrometer;
+
+import feign.AsyncClient;
+import feign.Capability;
+import feign.Client;
+import feign.FeignException;
+import feign.Response;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+
+/** Warp feign {@link Client} with metrics. */
+public class MicrometerObservationCapability implements Capability {
+
+ private final ObservationRegistry observationRegistry;
+
+ private final FeignObservationConvention customFeignObservationConvention;
+
+ public MicrometerObservationCapability(ObservationRegistry observationRegistry,
+ FeignObservationConvention customFeignObservationConvention) {
+ this.observationRegistry = observationRegistry;
+ this.customFeignObservationConvention = customFeignObservationConvention;
+ }
+
+ public MicrometerObservationCapability(ObservationRegistry observationRegistry) {
+ this(observationRegistry, null);
+ }
+
+ @Override
+ public Client enrich(Client client) {
+ return (request, options) -> {
+ FeignContext feignContext = new FeignContext(request);
+
+ Observation observation = FeignObservationDocumentation.DEFAULT
+ .observation(this.customFeignObservationConvention,
+ DefaultFeignObservationConvention.INSTANCE, () -> feignContext,
+ this.observationRegistry)
+ .start();
+
+ try {
+ Response response = client.execute(request, options);
+ finalizeObservation(feignContext, observation, null, response);
+ return response;
+ } catch (FeignException ex) {
+ finalizeObservation(feignContext, observation, ex, null);
+ throw ex;
+ }
+ };
+ }
+
+ @Override
+ public AsyncClient