Skip to content
This repository was archived by the owner on Dec 23, 2017. It is now read-only.

Commit 48af772

Browse files
committed
Merge pull request #8 from Graylog2/feature-opt-out
Add opt-out REST endpoints and service to disable sending stats
2 parents 44df03a + 44f0e1a commit 48af772

File tree

11 files changed

+625
-17
lines changed

11 files changed

+625
-17
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright (C) 2015 Graylog, Inc. ([email protected])
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.graylog.plugins.usagestatistics;
17+
18+
import com.squareup.okhttp.MediaType;
19+
import org.graylog2.plugin.Version;
20+
21+
public class UsageStatsConstants {
22+
public static final MediaType CONTENT_TYPE = MediaType.parse("application/x-jackson-smile");
23+
public static final String USAGE_STATS_VERSION = UsageStatsMetaData.VERSION.toString();
24+
public static final String USER_AGENT = "Graylog " + Version.CURRENT_CLASSPATH;
25+
}

src/main/java/org/graylog/plugins/usagestatistics/UsageStatsModule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ protected void configure() {
5555
addPeriodical(UsageStatsNodePeriodical.class);
5656
addPeriodical(UsageStatsClusterPeriodical.class);
5757
addRestResource(UsageStatsResource.class);
58+
addRestResource(UsageStatsOptOutResource.class);
5859

5960
addConfigBeans();
6061
}

src/main/java/org/graylog/plugins/usagestatistics/UsageStatsNodePeriodical.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131

3232
import javax.inject.Inject;
3333
import javax.inject.Singleton;
34-
import java.net.MalformedURLException;
3534
import java.net.URL;
3635

3736
@Singleton
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Copyright (C) 2015 Graylog, Inc. ([email protected])
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.graylog.plugins.usagestatistics;
17+
18+
import com.codahale.metrics.annotation.Timed;
19+
import com.wordnik.swagger.annotations.Api;
20+
import com.wordnik.swagger.annotations.ApiOperation;
21+
import com.wordnik.swagger.annotations.ApiResponse;
22+
import com.wordnik.swagger.annotations.ApiResponses;
23+
import org.apache.shiro.authz.annotation.RequiresAuthentication;
24+
import org.graylog2.plugin.rest.PluginRestResource;
25+
import org.graylog2.shared.rest.resources.RestResource;
26+
27+
import javax.inject.Inject;
28+
import javax.validation.Valid;
29+
import javax.validation.constraints.NotNull;
30+
import javax.ws.rs.Consumes;
31+
import javax.ws.rs.GET;
32+
import javax.ws.rs.POST;
33+
import javax.ws.rs.Path;
34+
import javax.ws.rs.Produces;
35+
import javax.ws.rs.core.MediaType;
36+
37+
@RequiresAuthentication
38+
@Api(value = "Usage Statistics Opt-Out", description = "Anonymous usage statistics opt-out state of this Graylog setup")
39+
@Path("/opt-out")
40+
public class UsageStatsOptOutResource extends RestResource implements PluginRestResource {
41+
private final UsageStatsOptOutService usageStatsOptOutService;
42+
43+
@Inject
44+
public UsageStatsOptOutResource(UsageStatsOptOutService usageStatsOptOutService) {
45+
this.usageStatsOptOutService = usageStatsOptOutService;
46+
}
47+
48+
@GET
49+
@Produces(MediaType.APPLICATION_JSON)
50+
@Timed
51+
@ApiOperation(value = "Get opt-out status")
52+
@ApiResponses(value = {
53+
@ApiResponse(code = 500, message = "Internal Server Error")
54+
})
55+
public UsageStatsOptOutState getOptOutState() {
56+
return usageStatsOptOutService.getOptOutState();
57+
}
58+
59+
@POST
60+
@Consumes(MediaType.APPLICATION_JSON)
61+
@Produces(MediaType.APPLICATION_JSON)
62+
@Timed
63+
@ApiOperation(value = "Disable sending anonymous usage stats")
64+
@ApiResponses(value = {
65+
@ApiResponse(code = 400, message = "Missing or invalid opt-out state"),
66+
@ApiResponse(code = 500, message = "Internal Server Error")
67+
})
68+
public void setOptOutState(@Valid @NotNull UsageStatsOptOutState optOutState) {
69+
usageStatsOptOutService.setOptOutState(optOutState);
70+
}
71+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* Copyright (C) 2015 Graylog, Inc. ([email protected])
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.graylog.plugins.usagestatistics;
17+
18+
import com.fasterxml.jackson.core.JsonProcessingException;
19+
import com.fasterxml.jackson.databind.ObjectMapper;
20+
import com.google.common.collect.ImmutableMap;
21+
import com.squareup.okhttp.Callback;
22+
import com.squareup.okhttp.Headers;
23+
import com.squareup.okhttp.HttpUrl;
24+
import com.squareup.okhttp.OkHttpClient;
25+
import com.squareup.okhttp.Request;
26+
import com.squareup.okhttp.RequestBody;
27+
import com.squareup.okhttp.Response;
28+
import org.graylog.plugins.usagestatistics.providers.SmileObjectMapper;
29+
import org.graylog2.plugin.cluster.ClusterConfigService;
30+
import org.graylog2.plugin.cluster.ClusterId;
31+
import org.slf4j.Logger;
32+
import org.slf4j.LoggerFactory;
33+
34+
import javax.annotation.Nullable;
35+
import javax.inject.Inject;
36+
import javax.ws.rs.core.HttpHeaders;
37+
import java.io.IOException;
38+
import java.net.URL;
39+
40+
import static org.graylog.plugins.usagestatistics.UsageStatsConstants.CONTENT_TYPE;
41+
import static org.graylog.plugins.usagestatistics.UsageStatsConstants.USAGE_STATS_VERSION;
42+
import static org.graylog.plugins.usagestatistics.UsageStatsConstants.USER_AGENT;
43+
44+
public class UsageStatsOptOutService {
45+
private static final Logger LOG = LoggerFactory.getLogger(UsageStatsOptOutService.class);
46+
47+
private final ClusterConfigService clusterConfigService;
48+
private final UsageStatsConfiguration config;
49+
private final OkHttpClient httpClient;
50+
private final ObjectMapper objectMapper;
51+
52+
@Inject
53+
public UsageStatsOptOutService(ClusterConfigService clusterConfigService,
54+
UsageStatsConfiguration config,
55+
OkHttpClient httpClient,
56+
@SmileObjectMapper ObjectMapper objectMapper) {
57+
this.clusterConfigService = clusterConfigService;
58+
this.config = config;
59+
this.httpClient = httpClient;
60+
this.objectMapper = objectMapper;
61+
}
62+
63+
public UsageStatsOptOutState getOptOutState() {
64+
return clusterConfigService.getOrDefault(UsageStatsOptOutState.class, UsageStatsOptOutState.create(false));
65+
}
66+
67+
public void setOptOutState(final UsageStatsOptOutState optOutState) {
68+
if (optOutState == null) {
69+
return;
70+
}
71+
72+
LOG.debug("Writing opt-out state to cluster config: {}", optOutState);
73+
clusterConfigService.write(optOutState);
74+
75+
if (optOutState.isOptOut()) {
76+
LOG.info("Transmission of anonymous usage stats: disabled (opt-out)");
77+
} else {
78+
LOG.info("Transmission of anonymous usage stats: enabled (opt-in)");
79+
LOG.debug("Not sending opt-in request.");
80+
return;
81+
}
82+
83+
final URL url = getUrl();
84+
85+
if (url == null) {
86+
LOG.debug("Not sending opt-out request, 'usage_statistics_url' is not set.");
87+
return;
88+
}
89+
90+
final Headers headers = new Headers.Builder()
91+
.add(HttpHeaders.USER_AGENT, USER_AGENT)
92+
.add("X-Usage-Statistics-Version", USAGE_STATS_VERSION)
93+
.build();
94+
95+
final Request request = new Request.Builder()
96+
.url(url)
97+
.headers(headers)
98+
.post(RequestBody.create(CONTENT_TYPE, buildPayload()))
99+
.build();
100+
101+
// Run the opt-out request outside of the calling thread so it does not block.
102+
httpClient.newCall(request).enqueue(new Callback() {
103+
@Override
104+
public void onFailure(Request request, IOException e) {
105+
LOG.error("Error while sending anonymous usage statistics opt-out");
106+
LOG.debug("Error details", e);
107+
}
108+
109+
@Override
110+
public void onResponse(Response response) throws IOException {
111+
if (!response.isSuccessful()) {
112+
LOG.warn("Couldn't successfully send usage statistics opt-out: {}", response);
113+
}
114+
}
115+
});
116+
}
117+
118+
@Nullable
119+
protected URL getUrl() {
120+
final ClusterId clusterId = clusterConfigService.get(ClusterId.class);
121+
122+
if (clusterId != null) {
123+
return HttpUrl.get(config.getUrl()).newBuilder()
124+
.addPathSegment("cluster")
125+
.addPathSegment(clusterId.clusterId())
126+
.addPathSegment("optout")
127+
.build()
128+
.url();
129+
} else {
130+
return null;
131+
}
132+
}
133+
134+
@Nullable
135+
protected byte[] buildPayload() {
136+
try {
137+
return objectMapper.writeValueAsBytes(ImmutableMap.<String, String>builder().build());
138+
} catch (JsonProcessingException e) {
139+
LOG.error("Error while serializing usage statistics data", e);
140+
return null;
141+
}
142+
}
143+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Copyright (C) 2015 Graylog, Inc. ([email protected])
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.graylog.plugins.usagestatistics;
17+
18+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
19+
import com.fasterxml.jackson.annotation.JsonCreator;
20+
import com.fasterxml.jackson.annotation.JsonProperty;
21+
import com.google.auto.value.AutoValue;
22+
23+
@JsonAutoDetect
24+
@AutoValue
25+
public abstract class UsageStatsOptOutState {
26+
@JsonProperty("opt_out")
27+
public abstract boolean isOptOut();
28+
29+
@JsonCreator
30+
public static UsageStatsOptOutState create(@JsonProperty("opt_out") boolean isOptOut) {
31+
return new AutoValue_UsageStatsOptOutState(isOptOut);
32+
}
33+
}

src/main/java/org/graylog/plugins/usagestatistics/UsageStatsPeriodical.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@
2121
import com.google.common.net.HttpHeaders;
2222
import com.google.common.primitives.Ints;
2323
import com.squareup.okhttp.Headers;
24-
import com.squareup.okhttp.MediaType;
2524
import com.squareup.okhttp.OkHttpClient;
2625
import com.squareup.okhttp.Request;
2726
import com.squareup.okhttp.RequestBody;
2827
import com.squareup.okhttp.Response;
29-
import org.graylog2.plugin.Version;
3028
import org.graylog2.plugin.cluster.ClusterConfigService;
3129
import org.graylog2.plugin.periodical.Periodical;
3230
import org.joda.time.DateTime;
@@ -41,12 +39,11 @@
4139
import java.util.List;
4240

4341
import static com.google.common.base.Preconditions.checkNotNull;
42+
import static org.graylog.plugins.usagestatistics.UsageStatsConstants.CONTENT_TYPE;
43+
import static org.graylog.plugins.usagestatistics.UsageStatsConstants.USAGE_STATS_VERSION;
44+
import static org.graylog.plugins.usagestatistics.UsageStatsConstants.USER_AGENT;
4445

4546
public abstract class UsageStatsPeriodical extends Periodical {
46-
private static final MediaType CONTENT_TYPE = MediaType.parse("application/x-jackson-smile");
47-
private static final String USAGE_STATS_VERSION = UsageStatsMetaData.VERSION.toString();
48-
private static final String USER_AGENT = "Graylog " + Version.CURRENT_CLASSPATH;
49-
5047
protected final UsageStatsConfiguration config;
5148
protected final ClusterConfigService clusterConfigService;
5249
protected final EvictingQueue<UsageStatsRequest> cachedRequestsQueue;
@@ -74,8 +71,24 @@ protected UsageStatsPeriodical(UsageStatsConfiguration config,
7471

7572
protected abstract URL getUrl();
7673

74+
protected boolean isEnabled() {
75+
if (!config.isEnabled()) {
76+
return false;
77+
}
78+
79+
final UsageStatsOptOutState state = clusterConfigService.getOrDefault(UsageStatsOptOutState.class,
80+
UsageStatsOptOutState.create(false));
81+
82+
return !state.isOptOut();
83+
}
84+
7785
@Override
7886
public void doRun() {
87+
if (!isEnabled()) {
88+
log.debug("Anonymous usage statistics disabled: Not transmitting statistics");
89+
return;
90+
}
91+
7992
log.debug("Anonymous usage statistics activated: Transmitting node statistics.");
8093
final byte[] requestBody = buildPayload();
8194

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Copyright (C) 2015 Graylog, Inc. ([email protected])
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.graylog.plugins.usagestatistics;
17+
18+
import com.google.common.collect.Maps;
19+
import org.graylog2.plugin.cluster.ClusterConfigService;
20+
import org.graylog2.shared.utilities.AutoValueUtils;
21+
22+
import java.util.Map;
23+
24+
import static com.google.common.base.MoreObjects.firstNonNull;
25+
26+
public class TestClusterConfigService implements ClusterConfigService {
27+
private final Map<String, Object> data = Maps.newConcurrentMap();
28+
29+
@SuppressWarnings("unchecked")
30+
public <T> T get(Class<T> type) {
31+
return (T) data.get(type.getCanonicalName());
32+
}
33+
34+
@Override
35+
public <T> T getOrDefault(Class<T> type, T defaultValue) {
36+
return firstNonNull(get(type), defaultValue);
37+
}
38+
39+
@Override
40+
public <T> void write(T payload) {
41+
data.put(AutoValueUtils.getCanonicalName(payload.getClass()), payload);
42+
}
43+
44+
public <T> void remove(Class<T> type) {
45+
data.remove(type.getCanonicalName());
46+
}
47+
48+
public void clear() {
49+
data.clear();
50+
}
51+
}

0 commit comments

Comments
 (0)