Skip to content
Open
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
1 change: 1 addition & 0 deletions API_CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* Introduce a new `org.sonarsource.sonarlint.core.rpc.protocol.backend.labs.IdeLabsRpcService` service and a `joinIdeLabsProgram` method.
* Use it to allow users to join the SonarQube for IDE Labs program
* The method accepts user email and IDE name as parameters
* Add a new `GESSIE_TELEMETRY` capability in `org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.BackendCapability`. Clients using the feature need to declare it at initialization time. Enables sending data to Gessie (Generic Event System) alongside previous telemetry implementation.

# 10.35

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
import org.sonarsource.sonarlint.core.tracking.TaintVulnerabilityTrackingService;
import org.sonarsource.sonarlint.core.tracking.TrackingService;
import org.sonarsource.sonarlint.core.websocket.WebSocketService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
Expand Down Expand Up @@ -224,6 +225,11 @@
})
public class SonarLintSpringAppConfig {

@Value("#{environment.SONARLINT_HTTP_MAX_RETRIES ?: 2}")
private Integer httpMaxRetries;
@Value("#{environment.SONARLINT_HTTP_RETRY_INTERVAL_SECONDS ?: 3}")
private Integer httpRetryInterval;

@Bean(name = "applicationEventMulticaster")
public ApplicationEventMulticaster simpleApplicationEventMulticaster() {
var eventMulticaster = new SimpleApplicationEventMulticaster();
Expand All @@ -247,7 +253,7 @@ SonarCloudActiveEnvironment provideSonarCloudActiveEnvironment(InitializeParams
HttpClientProvider provideHttpClientProvider(InitializeParams params, UserPaths userPaths, AskClientCertificatePredicate askClientCertificatePredicate,
ProxySelector proxySelector, CredentialsProvider proxyCredentialsProvider) {
return new HttpClientProvider(params.getClientConstantInfo().getUserAgent(), adapt(params.getHttpConfiguration(), userPaths.getUserHome()), askClientCertificatePredicate,
proxySelector, proxyCredentialsProvider);
proxySelector, proxyCredentialsProvider, httpMaxRetries, httpRetryInterval);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient;
import org.sonarsource.sonarlint.core.rpc.protocol.backend.initialize.InitializeParams;
import org.sonarsource.sonarlint.core.telemetry.TelemetrySpringConfig;
import org.sonarsource.sonarlint.core.telemetry.gessie.GessieSpringConfig;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

Expand All @@ -36,6 +37,7 @@ public SpringApplicationContextInitializer(SonarLintRpcClient client, Initialize
applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(SonarLintSpringAppConfig.class);
applicationContext.register(TelemetrySpringConfig.class);
applicationContext.register(GessieSpringConfig.class);
applicationContext.register(IdeLabsSpringConfig.class);
applicationContext.registerBean("sonarlintClient", SonarLintRpcClient.class, () -> requireNonNull(client));
applicationContext.registerBean("initializeParams", InitializeParams.class, () -> params);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* SonarLint Core - Implementation
* Copyright (C) 2016-2025 SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.core.telemetry.gessie;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({
GessieService.class,
GessieHttpClient.class
})
public class GessieSpringConfig {

public static final String PROPERTY_GESSIE_ENDPOINT = "sonarlint.internal.telemetry.gessie.endpoint";
public static final String PROPERTY_GESSIE_API_KEY = "sonarlint.internal.telemetry.gessie.api.key";
private static final String GESSIE_ENDPOINT = "https://events.sonardata.io";
private static final String IDE_SOURCE = "CiiwpdWnR21rWEOkgJ8tr3EYSXb7dzaQ5ezbipLb";

@Bean
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick but you can define a name with @Bean(name = xxx), just to make sure it doesn't break if someone changes the name of the method, as you prefer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would prefer to leave it. I like how Spring allows you to declare stuff without repeating yourself. And if somebody want to rename it they're better to do it everywhere anyway. So I would consider it a good thing that they're forced to do that.

String gessieEndpoint() {
return System.getProperty(PROPERTY_GESSIE_ENDPOINT, GESSIE_ENDPOINT);
}

@Bean
String gessieApiKey() {
return System.getProperty(PROPERTY_GESSIE_API_KEY, IDE_SOURCE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* SonarLint Core - Implementation
* Copyright (C) 2016-2025 SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
@ParametersAreNonnullByDefault
package org.sonarsource.sonarlint.core.telemetry.gessie;

import javax.annotation.ParametersAreNonnullByDefault;
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpResponse;
Expand All @@ -48,21 +49,29 @@ class ApacheHttpClientAdapter implements HttpClient {

private static final SonarLintLogger LOG = SonarLintLogger.get();
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String X_API_KEY_HEADER = "x-api-key";
private static final Timeout STREAM_CONNECTION_REQUEST_TIMEOUT = Timeout.ofSeconds(10);
private static final Timeout STREAM_CONNECTION_TIMEOUT = Timeout.ofMinutes(1);

private final CloseableHttpAsyncClient apacheClient;
@Nullable
private final String usernameOrToken;
@Nullable
private final String password;
private final boolean shouldUseBearer;
@Nullable
private final String xApiKey;
private final boolean withRetries;
private boolean connected = false;

private ApacheHttpClientAdapter(CloseableHttpAsyncClient apacheClient, @Nullable String usernameOrToken, @Nullable String password, boolean shouldUseBearer) {
private ApacheHttpClientAdapter(CloseableHttpAsyncClient apacheClient, @Nullable String usernameOrToken, @Nullable String password, boolean shouldUseBearer,
@Nullable String xApiKey, boolean withRetries) {
this.apacheClient = apacheClient;
this.usernameOrToken = usernameOrToken;
this.password = password;
this.shouldUseBearer = shouldUseBearer;
this.xApiKey = xApiKey;
this.withRetries = withRetries;
}

@Override
Expand Down Expand Up @@ -115,13 +124,7 @@ public AsyncRequest getEventStream(String url, HttpConnectionListener connection
.setResponseTimeout(Timeout.ZERO_MILLISECONDS)
.build());

if (usernameOrToken != null) {
if (shouldUseBearer) {
request.setHeader(AUTHORIZATION_HEADER, bearer(usernameOrToken));
} else {
request.setHeader(AUTHORIZATION_HEADER, basic(usernameOrToken, Objects.requireNonNullElse(password, "")));
}
}
setAuthHeader(request);
request.setHeader("Accept", "text/event-stream");
connected = false;
var cancelled = new AtomicBoolean();
Expand Down Expand Up @@ -203,13 +206,27 @@ public void cancelled() {
return new HttpAsyncRequest(httpFuture);
}

private void setAuthHeader(SimpleHttpRequest request) {
if (usernameOrToken != null) {
if (shouldUseBearer) {
request.setHeader(AUTHORIZATION_HEADER, bearer(usernameOrToken));
} else {
request.setHeader(AUTHORIZATION_HEADER, basic(usernameOrToken, Objects.requireNonNullElse(password, "")));
}
} else if (xApiKey != null) {
request.setHeader(X_API_KEY_HEADER, xApiKey);
}
}

private class CompletableFutureWrappingFuture extends CompletableFuture<HttpClient.Response> {

private final Future<SimpleHttpResponse> wrapped;

private CompletableFutureWrappingFuture(SimpleHttpRequest httpRequest) {
var callingThreadLogOutput = SonarLintLogger.get().getTargetForCopy();
this.wrapped = apacheClient.execute(httpRequest, new FutureCallback<>() {
var context = new HttpClientContext();
context.setAttribute(ContextAttributes.RETRIES_ENABLED, withRetries);
this.wrapped = apacheClient.execute(httpRequest, context, new FutureCallback<>() {
@Override
public void completed(SimpleHttpResponse result) {
SonarLintLogger.get().setTarget(callingThreadLogOutput);
Expand Down Expand Up @@ -256,13 +273,7 @@ public boolean cancel(boolean mayInterruptIfRunning) {

private CompletableFuture<Response> executeAsync(SimpleHttpRequest httpRequest) {
try {
if (usernameOrToken != null) {
if (shouldUseBearer) {
httpRequest.setHeader(AUTHORIZATION_HEADER, bearer(usernameOrToken));
} else {
httpRequest.setHeader(AUTHORIZATION_HEADER, basic(usernameOrToken, Objects.requireNonNullElse(password, "")));
}
}
setAuthHeader(httpRequest);
return new CompletableFutureWrappingFuture(httpRequest);
} catch (Exception e) {
throw new IllegalStateException("Unable to execute request: " + e.getMessage(), e);
Expand Down Expand Up @@ -304,16 +315,59 @@ public void cancel() {
}
}

public static ApacheHttpClientAdapter withoutCredentials(CloseableHttpAsyncClient apacheClient) {
return new ApacheHttpClientAdapter(apacheClient, null, null, false);
public static Builder builder() {
return new Builder();
}

public static ApacheHttpClientAdapter withUsernamePassword(CloseableHttpAsyncClient apacheClient, String username, @Nullable String password) {
return new ApacheHttpClientAdapter(apacheClient, username, password, false);
}
public static final class Builder {

private CloseableHttpAsyncClient apacheClient;
@Nullable
private String usernameOrToken;
@Nullable
private String password;
private boolean shouldUseBearer = false;
@Nullable
private String xApiKey;
private boolean withRetries = false;

public Builder withInnerClient(CloseableHttpAsyncClient apacheClient) {
this.apacheClient = apacheClient;
return this;
}

public static ApacheHttpClientAdapter withToken(CloseableHttpAsyncClient apacheClient, String token, boolean shouldUseBearer) {
return new ApacheHttpClientAdapter(apacheClient, token, null, shouldUseBearer);
}
public Builder withUserNamePassword(String username, @Nullable String password) {
this.usernameOrToken = username;
this.password = password;
return this;
}

public Builder withToken(String token) {
this.usernameOrToken = token;
return this;
}

public Builder useBearer(boolean shouldUseBearer) {
this.shouldUseBearer = shouldUseBearer;
return this;
}

public Builder withXApiKey(String xApiKey) {
this.xApiKey = xApiKey;
return this;
}

public Builder withRetries() {
this.withRetries = true;
return this;
}

ApacheHttpClientAdapter build() {
if (apacheClient == null) {
throw new IllegalStateException("Required an Apache HTTP client to wrap.");
}

return new ApacheHttpClientAdapter(apacheClient, usernameOrToken, password, shouldUseBearer, xApiKey, withRetries);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* SonarLint Core - HTTP
* Copyright (C) 2016-2025 SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.core.http;

public class ContextAttributes {

public static final String RETRIES_ENABLED = "retries.enabled";

private ContextAttributes() { }
}
Loading