-
Notifications
You must be signed in to change notification settings - Fork 1k
feat: Add OpenTelemetry instrumentation for ActiveJ HTTP server #13335
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
Merged
Merged
Changes from 56 commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
b52c49d
Introduces OpenTelemetry instrumentation for the ActiveJ framework, e…
kcsurapaneni 1cc6c5f
Resolved PR comments
kcsurapaneni b4e8e29
instrumentation name is updated with version
kcsurapaneni ce368c1
instrumentation name is updated with version
kcsurapaneni b287f78
instrumentation name is updated with version
kcsurapaneni e1a29dc
checkstyle config is reverted and added @SuppressWarnings(Abbreviatio…
kcsurapaneni 583e063
renames class files to avoid AbbreviationAsWordInName checkstyle rule
kcsurapaneni b7aff9d
Test cases are added
kcsurapaneni b241b45
build script dependencies is updated
kcsurapaneni 1662b23
throwable is now read-only parameter
kcsurapaneni 4fd797f
base version is set to Java 17
kcsurapaneni 7bf1776
PR comments addressed
kcsurapaneni a07317e
PR comments addressed
kcsurapaneni d5bb920
spotlessCheck applied
kcsurapaneni 429ad65
PR comments addressed
kcsurapaneni ac95594
explicitly added test dependencies
kcsurapaneni 5a4d2dd
Resolving common/check-latest-dep-test-overrides issue
kcsurapaneni c142d7f
fix double instrumentation (#13337)
zeitlinger c098084
Add comment to workflow file (#13343)
trask c0b67a3
Better qualify Java HttpClient instrumentation package name (#13296)
trask 4c149bd
Update apidiff baseline to released version 2.13.1 (#13348)
otelbot[bot] fea36de
Merge change log updates from release/v2.13.x (#13347)
otelbot[bot] 2c31d8f
fix(deps): update gradle develocity packages to v3.19.2 (patch) (#13349)
renovate[bot] 583801e
Fix automated PR body text (#13350)
trask f6ef942
fix(deps): update dependency io.opentelemetry.semconv:opentelemetry-s…
renovate[bot] bb75dab
fix(deps): update testcontainers-java monorepo to v1.20.5 (patch) (#1…
renovate[bot] aba23a3
fix(deps): update dependency com.google.apis:google-api-services-shee…
renovate[bot] 5a92938
Fix flaky test (#13358)
laurit 1420a18
Fix testLatestDeps (#13364)
trask 20d00a0
Fix TODO (#13363)
trask f1cfe0e
integration test case has been added which extends AbstractHttpServer…
kcsurapaneni 0748348
fix(deps): update dependency org.springframework.boot:spring-boot-sta…
renovate[bot] 6ec9373
fix(deps): update dependency org.testcontainers:testcontainers to v1.…
renovate[bot] 37032d2
Merge branch 'main' into activej
laurit 65e6d57
Test cases are updated to use JUnit 5
kcsurapaneni ad7692a
fix(deps): update dependency org.awaitility:awaitility to v4.3.0 (#13…
renovate[bot] 6f01a1c
Promise response method exit handling logic updated
kcsurapaneni dc94afa
addressed PR comments
kcsurapaneni 2332b12
fix(deps): update opentelemetry-java-contrib monorepo to v1.44.0-alph…
renovate[bot] fe1151c
[Spring Scheduling] Support Virtual Threads (#13370)
jakobjoachim 18b3cde
chore(deps): update weekly update (#13381)
renovate[bot] dfb73d1
fix(deps): update dependency checkstyle to v10.21.3 (#13380)
renovate[bot] f167d99
fix(deps): update junit5 monorepo to v5.12.0 (minor) (#13372)
renovate[bot] bf3fe1f
fix(deps): update dependency com.google.auth:google-auth-library-oaut…
renovate[bot] e802708
fix semconv naming for 'jvm.buffer.memory.used' metric (#13374)
SylvainJuge f6ee0dc
fix(deps): update dependency com.google.auth:google-auth-library-oaut…
renovate[bot] 036ae0b
chore(deps): update dependency gradle to v8.13 (#13394)
renovate[bot] 148a91a
Ensure tilde$1 onExit is run in correct order (#13360)
masonedmison 2f40b7f
Add instrumentation of AWS Bedrock to use gen_ai conventions (#13355)
anuraaga 483ec98
fix(deps): update dependency ch.qos.logback:logback-classic to v1.5.1…
renovate[bot] ef5ceec
Merge remote-tracking branch 'origin' into activej
kcsurapaneni 418a259
@laurit PR comments are addressed
kcsurapaneni 81827bc
@laurit PR comments are addressed
kcsurapaneni 87345f0
polish
laurit 9023ad8
reformat table
laurit 99a4c21
only consider 6.0+ classes
kcsurapaneni 7269a0e
supports minimum 6.0 version
kcsurapaneni File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
instrumentation/activej-http-6.0/javaagent/build.gradle.kts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| plugins { | ||
| id("otel.javaagent-instrumentation") | ||
| } | ||
|
|
||
| muzzle { | ||
| pass { | ||
| group.set("io.activej") | ||
| module.set("activej-http") | ||
| versions.set("[6.0,)") | ||
| assertInverse.set(true) | ||
| } | ||
| } | ||
|
|
||
| dependencies { | ||
| library("io.activej:activej-http:6.0-rc2") | ||
kcsurapaneni marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| latestDepTestLibrary("io.activej:activej-http:6.+") // documented limitation, can be removed when there is a non rc version in 6.x series | ||
| } | ||
|
|
||
| otelJava { | ||
| minJavaVersionSupported.set(JavaVersion.VERSION_17) | ||
| } | ||
90 changes: 90 additions & 0 deletions
90
...try/javaagent/instrumentation/activejhttp/ActivejHttpServerConnectionInstrumentation.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.javaagent.instrumentation.activejhttp; | ||
|
|
||
| import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; | ||
| import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; | ||
| import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasSuperType; | ||
| import static io.opentelemetry.javaagent.instrumentation.activejhttp.ActivejHttpServerConnectionSingletons.instrumenter; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isInterface; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isMethod; | ||
| import static net.bytebuddy.matcher.ElementMatchers.named; | ||
| import static net.bytebuddy.matcher.ElementMatchers.not; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArgument; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArguments; | ||
|
|
||
| import io.activej.http.AsyncServlet; | ||
| import io.activej.http.HttpRequest; | ||
| import io.activej.http.HttpResponse; | ||
| import io.activej.promise.Promise; | ||
| import io.opentelemetry.context.Context; | ||
| import io.opentelemetry.context.Scope; | ||
| import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; | ||
| import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; | ||
| import net.bytebuddy.asm.Advice; | ||
| import net.bytebuddy.description.type.TypeDescription; | ||
| import net.bytebuddy.matcher.ElementMatcher; | ||
|
|
||
| public class ActivejHttpServerConnectionInstrumentation implements TypeInstrumentation { | ||
|
|
||
| @Override | ||
| public ElementMatcher<TypeDescription> typeMatcher() { | ||
| return hasSuperType(named("io.activej.http.AsyncServlet")).and(not(isInterface())); | ||
kcsurapaneni marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| @Override | ||
| public ElementMatcher<ClassLoader> classLoaderOptimization() { | ||
| return hasClassesNamed("io.activej.http.AsyncServlet"); | ||
| } | ||
|
|
||
| @Override | ||
| public void transform(TypeTransformer transformer) { | ||
| transformer.applyAdviceToMethod( | ||
| isMethod() | ||
| .and(named("serve")) | ||
| .and(takesArguments(1).and(takesArgument(0, named("io.activej.http.HttpRequest")))), | ||
| this.getClass().getName() + "$ServeAdvice"); | ||
| } | ||
|
|
||
| @SuppressWarnings("unused") | ||
| public static class ServeAdvice { | ||
|
|
||
| @Advice.OnMethodEnter(suppress = Throwable.class) | ||
| public static void methodEnter( | ||
| @Advice.This AsyncServlet asyncServlet, | ||
| @Advice.Argument(0) HttpRequest request, | ||
| @Advice.Local("otelContext") Context context, | ||
| @Advice.Local("otelScope") Scope scope, | ||
| @Advice.Local("httpRequest") HttpRequest httpRequest) { | ||
| Context parentContext = currentContext(); | ||
| httpRequest = request; | ||
| if (!instrumenter().shouldStart(parentContext, request)) { | ||
| return; | ||
| } | ||
| context = instrumenter().start(parentContext, request); | ||
| scope = context.makeCurrent(); | ||
| } | ||
|
|
||
| @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) | ||
| public static void methodExit( | ||
| @Advice.This AsyncServlet asyncServlet, | ||
| @Advice.Return(readOnly = false) Promise<HttpResponse> responsePromise, | ||
| @Advice.Thrown Throwable throwable, | ||
| @Advice.Local("otelContext") Context context, | ||
| @Advice.Local("otelScope") Scope scope, | ||
| @Advice.Local("httpRequest") HttpRequest httpRequest) { | ||
| if (scope == null) { | ||
| return; | ||
| } | ||
| scope.close(); | ||
| if (throwable != null) { | ||
| instrumenter().end(context, httpRequest, null, throwable); | ||
| } else { | ||
| responsePromise = PromiseWrapper.wrap(responsePromise, httpRequest, context); | ||
| } | ||
| } | ||
| } | ||
| } | ||
33 changes: 33 additions & 0 deletions
33
...vaagent/instrumentation/activejhttp/ActivejHttpServerConnectionInstrumentationModule.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.javaagent.instrumentation.activejhttp; | ||
|
|
||
| import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; | ||
| import static java.util.Collections.singletonList; | ||
|
|
||
| import com.google.auto.service.AutoService; | ||
| import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; | ||
| import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; | ||
| import java.util.List; | ||
| import net.bytebuddy.matcher.ElementMatcher; | ||
|
|
||
| @AutoService(InstrumentationModule.class) | ||
| public class ActivejHttpServerConnectionInstrumentationModule extends InstrumentationModule { | ||
|
|
||
| public ActivejHttpServerConnectionInstrumentationModule() { | ||
| super("activej-http", "activej-http-6.0"); | ||
| } | ||
|
|
||
| @Override | ||
| public List<TypeInstrumentation> typeInstrumentations() { | ||
| return singletonList(new ActivejHttpServerConnectionInstrumentation()); | ||
| } | ||
|
|
||
| @Override | ||
| public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() { | ||
| return hasClassesNamed("io.activej.reactor.schedule.ScheduledPriorityQueue"); | ||
kcsurapaneni marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
32 changes: 32 additions & 0 deletions
32
...elemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerConnectionSingletons.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.javaagent.instrumentation.activejhttp; | ||
|
|
||
| import io.activej.http.HttpRequest; | ||
| import io.activej.http.HttpResponse; | ||
| import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; | ||
| import io.opentelemetry.javaagent.bootstrap.internal.JavaagentHttpServerInstrumenters; | ||
|
|
||
| public final class ActivejHttpServerConnectionSingletons { | ||
|
|
||
| private static final String INSTRUMENTATION_NAME = "io.opentelemetry.activej-http-6.0"; | ||
|
|
||
| private static final Instrumenter<HttpRequest, HttpResponse> INSTRUMENTER; | ||
|
|
||
| static { | ||
| INSTRUMENTER = | ||
| JavaagentHttpServerInstrumenters.create( | ||
| INSTRUMENTATION_NAME, | ||
| new ActivejHttpServerHttpAttributesGetter(), | ||
| ActivejHttpServerRequestGetter.INSTANCE); | ||
| } | ||
|
|
||
| public static Instrumenter<HttpRequest, HttpResponse> instrumenter() { | ||
| return INSTRUMENTER; | ||
| } | ||
|
|
||
| private ActivejHttpServerConnectionSingletons() {} | ||
| } |
102 changes: 102 additions & 0 deletions
102
...elemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerHttpAttributesGetter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.javaagent.instrumentation.activejhttp; | ||
|
|
||
| import io.activej.http.HttpHeader; | ||
| import io.activej.http.HttpHeaderValue; | ||
| import io.activej.http.HttpHeaders; | ||
| import io.activej.http.HttpRequest; | ||
| import io.activej.http.HttpResponse; | ||
| import io.opentelemetry.instrumentation.api.semconv.http.HttpServerAttributesGetter; | ||
| import java.net.InetAddress; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import javax.annotation.Nullable; | ||
|
|
||
| final class ActivejHttpServerHttpAttributesGetter | ||
| implements HttpServerAttributesGetter<HttpRequest, HttpResponse> { | ||
|
|
||
| @Override | ||
| public String getHttpRequestMethod(HttpRequest request) { | ||
| return request.getMethod().name(); | ||
| } | ||
|
|
||
| @Override | ||
| public List<String> getHttpRequestHeader(HttpRequest request, String name) { | ||
| HttpHeader httpHeader = HttpHeaders.of(name); | ||
| List<String> values = new ArrayList<>(); | ||
| for (Map.Entry<HttpHeader, HttpHeaderValue> entry : request.getHeaders()) { | ||
| if (httpHeader.equals(entry.getKey())) { | ||
| values.add(entry.getValue().toString()); | ||
| } | ||
| } | ||
|
|
||
| return values; | ||
| } | ||
|
|
||
| @Override | ||
| public Integer getHttpResponseStatusCode( | ||
| HttpRequest request, HttpResponse httpResponse, @Nullable Throwable error) { | ||
| return httpResponse.getCode(); | ||
| } | ||
|
|
||
| @Override | ||
| public List<String> getHttpResponseHeader( | ||
| HttpRequest request, HttpResponse httpResponse, String name) { | ||
| HttpHeader httpHeader = HttpHeaders.of(name); | ||
| List<String> values = new ArrayList<>(); | ||
| for (Map.Entry<HttpHeader, HttpHeaderValue> entry : httpResponse.getHeaders()) { | ||
| if (httpHeader.equals(entry.getKey())) { | ||
| values.add(entry.getValue().toString()); | ||
| } | ||
| } | ||
|
|
||
| return values; | ||
| } | ||
|
|
||
| @Override | ||
| public String getUrlScheme(HttpRequest request) { | ||
| return request.getProtocol().lowercase(); | ||
| } | ||
|
|
||
| @Override | ||
| public String getUrlPath(HttpRequest request) { | ||
| return request.getPath(); | ||
| } | ||
|
|
||
| @Override | ||
| public String getUrlQuery(HttpRequest request) { | ||
| return request.getQuery(); | ||
| } | ||
|
|
||
| @Override | ||
| public String getNetworkProtocolName(HttpRequest request, @Nullable HttpResponse httpResponse) { | ||
| return switch (request.getVersion()) { | ||
| case HTTP_0_9, HTTP_1_0, HTTP_1_1, HTTP_2_0 -> "http"; | ||
| default -> null; | ||
| }; | ||
| } | ||
|
|
||
| @Override | ||
| public String getNetworkProtocolVersion( | ||
| HttpRequest request, @Nullable HttpResponse httpResponse) { | ||
| return switch (request.getVersion()) { | ||
| case HTTP_0_9 -> "0.9"; | ||
| case HTTP_1_0 -> "1.0"; | ||
| case HTTP_1_1 -> "1.1"; | ||
| case HTTP_2_0 -> "2"; | ||
| default -> null; | ||
| }; | ||
| } | ||
|
|
||
| @Nullable | ||
| @Override | ||
| public String getNetworkPeerAddress(HttpRequest request, @Nullable HttpResponse httpResponse) { | ||
| InetAddress remoteAddress = request.getConnection().getRemoteAddress(); | ||
| return remoteAddress != null ? remoteAddress.getHostAddress() : null; | ||
| } | ||
| } |
51 changes: 51 additions & 0 deletions
51
...o/opentelemetry/javaagent/instrumentation/activejhttp/ActivejHttpServerRequestGetter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.javaagent.instrumentation.activejhttp; | ||
|
|
||
| import io.activej.http.HttpHeader; | ||
| import io.activej.http.HttpHeaderValue; | ||
| import io.activej.http.HttpHeaders; | ||
| import io.activej.http.HttpRequest; | ||
| import io.opentelemetry.context.propagation.internal.ExtendedTextMapGetter; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.Iterator; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| enum ActivejHttpServerRequestGetter implements ExtendedTextMapGetter<HttpRequest> { | ||
| INSTANCE; | ||
|
|
||
| @Override | ||
| public Iterable<String> keys(HttpRequest httpRequest) { | ||
| return httpRequest.getHeaders().stream().map(h -> h.getKey().toString()).toList(); | ||
| } | ||
|
|
||
| @Override | ||
| public String get(HttpRequest carrier, String key) { | ||
| if (carrier == null) { | ||
| return null; | ||
| } | ||
|
|
||
| return carrier.getHeader(HttpHeaders.of(key)); | ||
| } | ||
|
|
||
| @Override | ||
| public Iterator<String> getAll(HttpRequest carrier, String key) { | ||
| if (carrier == null) { | ||
| return Collections.emptyIterator(); | ||
| } | ||
|
|
||
| HttpHeader httpHeader = HttpHeaders.of(key); | ||
| List<String> values = new ArrayList<>(); | ||
| for (Map.Entry<HttpHeader, HttpHeaderValue> entry : carrier.getHeaders()) { | ||
| if (httpHeader.equals(entry.getKey())) { | ||
| values.add(entry.getValue().toString()); | ||
| } | ||
| } | ||
| return values.iterator(); | ||
| } | ||
| } |
25 changes: 25 additions & 0 deletions
25
.../src/main/java/io/opentelemetry/javaagent/instrumentation/activejhttp/PromiseWrapper.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /* | ||
| * Copyright The OpenTelemetry Authors | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| package io.opentelemetry.javaagent.instrumentation.activejhttp; | ||
|
|
||
| import static io.opentelemetry.javaagent.instrumentation.activejhttp.ActivejHttpServerConnectionSingletons.instrumenter; | ||
|
|
||
| import io.activej.http.HttpRequest; | ||
| import io.activej.http.HttpResponse; | ||
| import io.activej.promise.Promise; | ||
| import io.opentelemetry.context.Context; | ||
|
|
||
| public final class PromiseWrapper { | ||
|
|
||
| public static Promise<HttpResponse> wrap( | ||
| Promise<HttpResponse> promise, HttpRequest httpRequest, Context context) { | ||
| return promise.whenComplete( | ||
| (httpResponse, exception) -> | ||
| instrumenter().end(context, httpRequest, httpResponse, exception)); | ||
| } | ||
|
|
||
| private PromiseWrapper() {} | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.