Skip to content

Commit 1bee9f5

Browse files
committed
Implement metric collection
1 parent 6c9924b commit 1bee9f5

File tree

16 files changed

+778
-0
lines changed

16 files changed

+778
-0
lines changed

anvil-api/src/main/kotlin/org/anvilpowered/anvil/api/plugin/PluginInfo.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,6 @@ interface PluginInfo: Named {
3838
val organizationName: String
3939

4040
val buildDate: String
41+
42+
val metricIds: Map<String, Int>
4143
}

anvil-bungee/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies {
1414
}
1515

1616
implementation bungee
17+
implementation bstats
1718
implementation configurate_hocon
1819
implementation javasisst
1920
implementation(kotlin_reflect + ":" + kotlin_version)
@@ -50,6 +51,7 @@ shadowJar {
5051
include dependency(apache_commons)
5152
include dependency(aopalliance)
5253
include dependency(bson)
54+
include dependency(bstats)
5355
include dependency(configurate_core)
5456
include dependency(configurate_hocon)
5557
include dependency(guice)
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/*
2+
* Anvil - AnvilPowered
3+
* Copyright (C) 2020-2021
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Lesser General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package org.anvilpowered.anvil.bungee.metric
20+
21+
import com.google.gson.JsonArray
22+
import com.google.gson.JsonObject
23+
import com.google.inject.Inject
24+
import java.io.BufferedReader
25+
import java.io.BufferedWriter
26+
import java.io.ByteArrayOutputStream
27+
import java.io.DataOutputStream
28+
import java.io.File
29+
import java.io.FileReader
30+
import java.io.FileWriter
31+
import java.io.IOException
32+
import java.io.InputStreamReader
33+
import java.lang.reflect.InvocationTargetException
34+
import java.net.URL
35+
import java.nio.charset.StandardCharsets
36+
import java.util.UUID
37+
import java.util.concurrent.TimeUnit
38+
import java.util.zip.GZIPOutputStream
39+
import javax.net.ssl.HttpsURLConnection
40+
import net.md_5.bungee.api.ProxyServer
41+
import net.md_5.bungee.api.plugin.Plugin
42+
import net.md_5.bungee.config.Configuration
43+
import net.md_5.bungee.config.ConfigurationProvider
44+
import net.md_5.bungee.config.YamlConfiguration
45+
import org.anvilpowered.anvil.api.Environment
46+
import org.anvilpowered.anvil.common.metric.MetricService
47+
import org.slf4j.Logger
48+
49+
class BungeeMetricService @Inject constructor(
50+
private val logger: Logger
51+
) : MetricService {
52+
53+
private lateinit var environment: Environment
54+
private var serviceId = 0
55+
private var enabled = false
56+
private var serverUUID: String? = null
57+
private var logFailedRequests = false
58+
private var logSentData = false
59+
private var logResponseStatusText = false
60+
private val knownMetricsInstances: MutableList<Any> = ArrayList()
61+
62+
override fun initialize(env: Environment) {
63+
val bungeeId: Int? = env.pluginInfo.metricIds["bungeecord"]
64+
requireNotNull(bungeeId) { "Could not find a valid Metrics Id for BungeeCord. Please check your PluginInfo" }
65+
initialize(env, bungeeId)
66+
}
67+
68+
private fun initialize(environment: Environment, serviceId: Int) {
69+
this.environment = environment
70+
this.serviceId = serviceId
71+
try {
72+
loadConfig()
73+
} catch (e: IOException) {
74+
logger.error("Failed to load bStats config!", e)
75+
return
76+
}
77+
78+
if (!enabled) {
79+
return
80+
}
81+
82+
val usedMetricsClass = getFirstBStatsClass() ?: return
83+
if (usedMetricsClass == javaClass) {
84+
linkMetrics(this)
85+
startSubmitting()
86+
} else {
87+
val logMsg = "Failed to link to first metrics class ${usedMetricsClass.name}"
88+
try {
89+
usedMetricsClass.getMethod("linkMetrics", Any::class.java).invoke(null, this)
90+
} catch (e: NoSuchMethodException) {
91+
if (logFailedRequests) {
92+
logger.error(logMsg, e)
93+
}
94+
} catch (e: IllegalAccessException) {
95+
if (logFailedRequests) {
96+
logger.error(logMsg, e)
97+
}
98+
} catch (e: InvocationTargetException) {
99+
if (logFailedRequests) {
100+
logger.error(logMsg, e)
101+
}
102+
}
103+
}
104+
}
105+
106+
private fun linkMetrics(metrics: Any) = knownMetricsInstances.add(metrics)
107+
108+
private fun startSubmitting() {
109+
val initialDelay = (1000 * 60 * (3 + Math.random() * 3)).toLong()
110+
val secondDelay = (1000 * 60 * (Math.random() * 30)).toLong()
111+
val plugin = environment.plugin as Plugin
112+
ProxyServer.getInstance().scheduler.schedule(plugin, { submitData() }, initialDelay, TimeUnit.MILLISECONDS)
113+
ProxyServer.getInstance().scheduler.schedule(
114+
plugin, { submitData() }, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS
115+
)
116+
}
117+
118+
private fun getServerData(): JsonObject {
119+
val proxyInstance = ProxyServer.getInstance()
120+
val data = JsonObject()
121+
data.addProperty("serverUUID", serverUUID)
122+
data.addProperty("playerAmount", proxyInstance.onlineCount)
123+
data.addProperty("managedServers", proxyInstance.servers.size)
124+
data.addProperty("onlineMode", if (proxyInstance.config.isOnlineMode) 1 else 0)
125+
data.addProperty("bungeecordVersion", proxyInstance.version)
126+
data.addProperty("javaVersion", System.getProperty("java.version"))
127+
data.addProperty("osName", System.getProperty("os.name"))
128+
data.addProperty("osArch", System.getProperty("os.arch"))
129+
data.addProperty("osVersion", System.getProperty("os.version"))
130+
data.addProperty("coreCount", Runtime.getRuntime().availableProcessors())
131+
return data
132+
}
133+
134+
private fun submitData() {
135+
val data: JsonObject = getServerData()
136+
val pluginData = JsonArray()
137+
for (metrics in knownMetricsInstances) {
138+
try {
139+
val plugin = metrics.javaClass.getMethod("getPluginData").invoke(metrics)
140+
if (plugin is JsonObject) {
141+
pluginData.add(plugin)
142+
}
143+
} catch (ignored: Exception) {
144+
}
145+
}
146+
data.add("plugins", pluginData)
147+
try {
148+
sendData(data)
149+
} catch (e: Exception) {
150+
if (logFailedRequests) {
151+
logger.error("Could not submit plugin stats!", e)
152+
}
153+
}
154+
}
155+
156+
@Throws(IOException::class)
157+
private fun loadConfig() {
158+
val bStatsFolder = File("plugins/bStats")
159+
check(bStatsFolder.mkdirs()) { "Could not create the config directory for bStats!" }
160+
val configFile = File(bStatsFolder, "config.yml")
161+
check(configFile.mkdirs()) { "Could not create the config file for bStats!" }
162+
163+
writeFile(
164+
configFile,
165+
"#bStats collects some data for plugin authors like how many servers are using their plugins.",
166+
"#To honor their work, you should not disable it.",
167+
"#This has nearly no effect on the server performance!",
168+
"#Check out https://bStats.org/ to learn more :)",
169+
"enabled: true",
170+
"serverUuid: \"" + UUID.randomUUID() + "\"",
171+
"logFailedRequests: false",
172+
"logSentData: false",
173+
"logResponseStatusText: false"
174+
)
175+
176+
val configuration: Configuration = ConfigurationProvider.getProvider(YamlConfiguration::class.java).load(configFile)
177+
178+
enabled = configuration.getBoolean("enabled", true)
179+
serverUUID = configuration.getString("serverUuid")
180+
logFailedRequests = configuration.getBoolean("logFailedRequests", false)
181+
logSentData = configuration.getBoolean("logSentData", false)
182+
logResponseStatusText = configuration.getBoolean("logResponseStatusText", false)
183+
}
184+
185+
private fun getFirstBStatsClass(): Class<*>? {
186+
val bStatsFolder = File("plugins/bStats")
187+
bStatsFolder.mkdirs()
188+
check(bStatsFolder.mkdirs()) { "Could not create the bStats config folder!" }
189+
val tempFile = File(bStatsFolder, "temp.txt")
190+
return try {
191+
val className = readFile(tempFile)
192+
if (className != null) {
193+
try {
194+
return Class.forName(className)
195+
} catch (ignored: ClassNotFoundException) {
196+
}
197+
}
198+
writeFile(tempFile, javaClass.name)
199+
javaClass
200+
} catch (e: IOException) {
201+
if (logFailedRequests) {
202+
logger.error("Failed to get first bStats class!", e)
203+
}
204+
null
205+
}
206+
}
207+
208+
@Throws(IOException::class)
209+
private fun readFile(file: File): String? {
210+
return if (!file.exists()) {
211+
null
212+
} else {
213+
BufferedReader(FileReader(file)).use { it.readLine() }
214+
}
215+
}
216+
217+
@Throws(IOException::class)
218+
private fun writeFile(file: File, vararg lines: String) {
219+
BufferedWriter(FileWriter(file)).use {
220+
for (line in lines) {
221+
it.write(line)
222+
it.newLine()
223+
}
224+
}
225+
}
226+
227+
private fun sendData(data: JsonObject?) {
228+
requireNotNull(data) { "Data cannot be null" }
229+
if (logSentData) {
230+
logger.info("Sending data to bStats: $data")
231+
}
232+
val connection: HttpsURLConnection = URL("https://bStats.org/submitData/bungeecord").openConnection() as HttpsURLConnection
233+
val compressedData = compress(data.toString())
234+
235+
connection.requestMethod = "POST"
236+
connection.addRequestProperty("Accept", "application/json")
237+
connection.addRequestProperty("Connection", "close")
238+
connection.addRequestProperty("Content-Encoding", "gzip")
239+
connection.addRequestProperty("Content-Length", compressedData!!.size.toString())
240+
connection.setRequestProperty("Content-Type", "application/json")
241+
connection.setRequestProperty("User-Agent", "MC-Server/1")
242+
243+
connection.doOutput = true
244+
DataOutputStream(connection.outputStream).use { outputStream -> outputStream.write(compressedData) }
245+
val builder = StringBuilder()
246+
BufferedReader(InputStreamReader(connection.inputStream)).use { bufferedReader ->
247+
var line: String?
248+
while (bufferedReader.readLine().also { line = it } != null) {
249+
builder.append(line)
250+
}
251+
}
252+
if (logResponseStatusText) {
253+
logger.info("Sent data to bStats and received response: $builder")
254+
}
255+
}
256+
257+
private fun compress(str: String?): ByteArray? {
258+
if (str == null) {
259+
return null
260+
}
261+
val outputStream = ByteArrayOutputStream()
262+
GZIPOutputStream(outputStream).use { gzip -> gzip.write(str.toByteArray(StandardCharsets.UTF_8)) }
263+
return outputStream.toByteArray()
264+
}
265+
}

anvil-common/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ plugins {
88
dependencies {
99
api(project(':anvil-api'))
1010

11+
api(bstats)
1112
api("net.kyori:adventure-text-serializer-legacy:4.5.0")
1213
api("net.kyori:adventure-text-serializer-plain:4.5.0")
1314
api(configurate_hocon)

anvil-common/src/main/java/org/anvilpowered/anvil/api/EnvironmentBuilderImpl.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.common.base.Preconditions;
2222
import com.google.common.reflect.TypeToken;
2323
import com.google.inject.AbstractModule;
24+
import com.google.inject.Binding;
2425
import com.google.inject.Guice;
2526
import com.google.inject.Injector;
2627
import com.google.inject.Key;
@@ -32,6 +33,7 @@
3233
import org.anvilpowered.anvil.api.registry.Registry;
3334
import org.anvilpowered.anvil.api.registry.RegistryScope;
3435
import org.anvilpowered.anvil.common.PlatformImpl;
36+
import org.anvilpowered.anvil.common.metric.MetricService;
3537
import org.anvilpowered.anvil.common.module.PlatformModule;
3638
import org.checkerframework.checker.nullness.qual.Nullable;
3739

@@ -132,6 +134,11 @@ protected void configure() {
132134
}
133135
ServiceManagerImpl.environmentManager
134136
.registerEnvironment(environment, environment.getPlugin());
137+
Binding<MetricService> metricBinding =
138+
Anvil.environment.getInjector().getExistingBinding(Key.get(MetricService.class));
139+
if (metricBinding != null) {
140+
metricBinding.getProvider().get().initialize(environment);
141+
}
135142
for (Map.Entry<Key<?>, Consumer<?>> entry
136143
: environment.getEarlyServices().entrySet()) {
137144
((Consumer) entry.getValue())

anvil-common/src/main/java/org/anvilpowered/anvil/common/plugin/AnvilPluginInfo.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818

1919
package org.anvilpowered.anvil.common.plugin;
2020

21+
import com.google.common.collect.ImmutableMap;
2122
import com.google.inject.Inject;
2223
import net.kyori.adventure.text.Component;
2324
import org.anvilpowered.anvil.api.plugin.PluginInfo;
2425
import org.anvilpowered.anvil.api.util.TextService;
26+
import org.jetbrains.annotations.NotNull;
27+
28+
import java.util.Map;
2529

2630
public class AnvilPluginInfo<TCommandSource> implements PluginInfo {
2731
public static final String id = "anvil";
@@ -33,6 +37,7 @@ public class AnvilPluginInfo<TCommandSource> implements PluginInfo {
3337
public static final String[] authors = {organizationName};
3438
public static final String buildDate = "$buildDate";
3539
public Component pluginPrefix;
40+
public static final Map<String, Integer> metricId = ImmutableMap.of();
3641

3742
@Inject
3843
public void setPluginPrefix(TextService<TCommandSource> textService) {
@@ -83,4 +88,9 @@ public String getBuildDate() {
8388
public Component getPrefix() {
8489
return pluginPrefix;
8590
}
91+
92+
@Override
93+
public Map<String, Integer> getMetricIds() {
94+
return metricId;
95+
}
8696
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Anvil - AnvilPowered
3+
* Copyright (C) 2020-2021
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Lesser General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package org.anvilpowered.anvil.common.metric
20+
21+
import org.anvilpowered.anvil.api.Environment
22+
23+
interface MetricService {
24+
25+
/**
26+
* Initializes metrics through bStats for the specified [Environment]
27+
*/
28+
fun initialize(env: Environment)
29+
30+
}

0 commit comments

Comments
 (0)