Browse Source

Serve http metrics from collectors

isundil 4 years ago
parent
commit
cc36554468

+ 3 - 0
.idea/.gitignore

@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml

+ 1 - 0
.idea/.name

@@ -0,0 +1 @@
+Prometheus android exporter

+ 116 - 0
.idea/codeStyles/Project.xml

@@ -0,0 +1,116 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <codeStyleSettings language="XML">
+      <indentOptions>
+        <option name="CONTINUATION_INDENT_SIZE" value="4" />
+      </indentOptions>
+      <arrangement>
+        <rules>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>xmlns:android</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>xmlns:.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*:id</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*:name</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>name</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>style</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>ANDROID_ATTRIBUTE_ORDER</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>.*</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+        </rules>
+      </arrangement>
+    </codeStyleSettings>
+  </code_scheme>
+</component>

+ 6 - 0
.idea/compiler.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <bytecodeTargetLevel target="1.8" />
+  </component>
+</project>

+ 21 - 0
.idea/gradle.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="GradleMigrationSettings" migrationVersion="1" />
+  <component name="GradleSettings">
+    <option name="linkedExternalProjectsSettings">
+      <GradleProjectSettings>
+        <option name="testRunner" value="PLATFORM" />
+        <option name="distributionType" value="DEFAULT_WRAPPED" />
+        <option name="externalProjectPath" value="$PROJECT_DIR$" />
+        <option name="modules">
+          <set>
+            <option value="$PROJECT_DIR$" />
+            <option value="$PROJECT_DIR$/app" />
+          </set>
+        </option>
+        <option name="resolveModulePerSourceSet" value="false" />
+        <option name="useQualifiedModuleNames" value="true" />
+      </GradleProjectSettings>
+    </option>
+  </component>
+</project>

+ 25 - 0
.idea/jarRepositories.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RemoteRepositoriesConfiguration">
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Maven Central repository" />
+      <option name="url" value="https://repo1.maven.org/maven2" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="jboss.community" />
+      <option name="name" value="JBoss Community repository" />
+      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="BintrayJCenter" />
+      <option name="name" value="BintrayJCenter" />
+      <option name="url" value="https://jcenter.bintray.com/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="Google" />
+      <option name="name" value="Google" />
+      <option name="url" value="https://dl.google.com/dl/android/maven2/" />
+    </remote-repository>
+  </component>
+</project>

+ 9 - 0
.idea/misc.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/build/classes" />
+  </component>
+  <component name="ProjectType">
+    <option name="id" value="Android" />
+  </component>
+</project>

+ 12 - 0
.idea/runConfigurations.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RunConfigurationProducerService">
+    <option name="ignoredProducers">
+      <set>
+        <option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
+        <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
+        <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
+      </set>
+    </option>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

BIN
app/libs/http-2.2.1.jar


BIN
app/libs/sun-common-server.jar


+ 66 - 0
app/src/main/java/info/knacki/prometheusandroidexporter/CollectorManager.java

@@ -0,0 +1,66 @@
+package info.knacki.prometheusandroidexporter;
+
+import androidx.annotation.NonNull;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+public class CollectorManager implements Iterable<CollectorValue> {
+    private static CollectorManager sInstance = null;
+    private final HashMap<String, ICollector> fCollectors;
+    private HashMap<String, Collection<CollectorValue>> fValues;
+
+    private CollectorManager(){
+        fCollectors = new HashMap<>();
+        tick();
+    }
+
+    public void RegisterCollector(ICollector collector) {
+        fCollectors.put(collector.getClass().getName(), collector);
+    }
+
+    public void tick() {
+        HashMap<String, Collection<CollectorValue>> values = new HashMap<>();
+
+        for (HashMap.Entry<String, ICollector> i : fCollectors.entrySet()) {
+            values.put(i.getKey(), i.getValue().ReadValues());
+        }
+        fValues = values;
+    }
+
+    public static CollectorManager GetInstance() {
+        synchronized (CollectorManager.class) {
+            if (sInstance == null) {
+                synchronized (CollectorManager.class) {
+                    return sInstance = new CollectorManager();
+                }
+            }
+            return sInstance;
+        }
+    }
+
+    @NonNull
+    @Override
+    public Iterator<CollectorValue> iterator() {
+        return new Iterator<CollectorValue>() {
+            private final Iterator<Map.Entry<String, Collection<CollectorValue>>> mainIterator = fValues.entrySet().iterator();
+            private Iterator<CollectorValue> secondIterator = null;
+
+            @Override
+            public boolean hasNext() {
+                if (secondIterator == null)
+                    return mainIterator.hasNext();
+                return secondIterator.hasNext() || mainIterator.hasNext();
+            }
+
+            @Override
+            public CollectorValue next() {
+                if (secondIterator == null || secondIterator.hasNext())
+                    secondIterator = mainIterator.next().getValue().iterator();
+                return secondIterator.next();
+            }
+        };
+    }
+}

+ 45 - 0
app/src/main/java/info/knacki/prometheusandroidexporter/CollectorValue.java

@@ -0,0 +1,45 @@
+package info.knacki.prometheusandroidexporter;
+
+import androidx.annotation.NonNull;
+
+public class CollectorValue {
+    public static class Type {
+        public final String fName;
+        private Type(String name) { this.fName = name; }
+
+        public final static Type COUNTER = new Type("counter");
+        public final static Type GAUGE = new Type("gauge");
+        public final static Type SUMMARY = new Type("summary");
+    }
+
+    public final String fName;
+    public final String fHelp;
+    public final Type fType;
+    private String valueAsString;
+
+    public CollectorValue(String name, String help, Type type) {
+        fName = name;
+        fHelp = help;
+        fType = type;
+    }
+
+    public CollectorValue(CollectorValue other) {
+        fName = other.fName;
+        fHelp = other.fHelp;
+        fType = other.fType;
+    }
+
+    public void SetValue(String value) {
+        valueAsString = value;
+    }
+
+    public void SetValue(double value) {
+        valueAsString = Double.toString(value);
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return valueAsString;
+    }
+}

+ 61 - 0
app/src/main/java/info/knacki/prometheusandroidexporter/CollectorWorker.java

@@ -0,0 +1,61 @@
+package info.knacki.prometheusandroidexporter;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.work.ListenableWorker;
+import androidx.work.WorkerParameters;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class CollectorWorker extends ListenableWorker {
+    /**
+     * @param appContext   The application {@link Context}
+     * @param workerParams Parameters to setup the internal state of this worker
+     */
+    public CollectorWorker(@NonNull Context appContext, @NonNull WorkerParameters workerParams) {
+        super(appContext, workerParams);
+    }
+
+    @NonNull
+    @Override
+    public ListenableFuture<Result> startWork() {
+        CollectorManager.GetInstance().tick();
+        return new ListenableFuture<Result>() {
+            @Override
+            public void addListener(Runnable listener, Executor executor) {
+
+            }
+
+            @Override
+            public boolean cancel(boolean mayInterruptIfRunning) {
+                return false;
+            }
+
+            @Override
+            public boolean isCancelled() {
+                return false;
+            }
+
+            @Override
+            public boolean isDone() {
+                return true;
+            }
+
+            @Override
+            public Result get() throws ExecutionException, InterruptedException {
+                return Result.success();
+            }
+
+            @Override
+            public Result get(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException {
+                return Result.success();
+            }
+        };
+    }
+}

+ 92 - 0
app/src/main/java/info/knacki/prometheusandroidexporter/HttpService.java

@@ -0,0 +1,92 @@
+package info.knacki.prometheusandroidexporter;
+
+import android.content.Context;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.Charset;
+import java.util.concurrent.Executors;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class HttpService {
+    public final static Logger gLogger = Logger.getLogger(HttpService.class.getName());
+    public static final short PORT = 8080;
+    private static final HttpService gService = new HttpService();
+    private HttpServer fServer = null;
+
+    public static void Register(Context ctx) throws IOException {
+        if (gService.fServer == null) {
+            gService.fServer = HttpServer.create(new InetSocketAddress(PORT), 0);
+            gService.fServer.setExecutor(Executors.newCachedThreadPool());
+            gService.fServer.createContext("/", new HttpHandler() {
+                @Override
+                public void handle(HttpExchange httpExchange) {
+                    try {
+                        gService.ServeRoot(httpExchange);
+                    }
+                    catch (IOException e) {
+                        gLogger.log(Level.SEVERE, "Cannot serve url /", e);
+                    }
+                }
+            });
+            gService.fServer.createContext("/metrics", new HttpHandler() {
+                @Override
+                public void handle(HttpExchange httpExchange) {
+                    try {
+                        gService.ServeMetrics(httpExchange);
+                    }
+                    catch (IOException e) {
+                        gLogger.log(Level.SEVERE, "Cannot serve url /", e);
+                    }
+                }
+            });
+            gService.fServer.start();
+        }
+    }
+
+    private void SendResponse(HttpExchange exh, int code, String contentType, String response) throws IOException {
+        exh.sendResponseHeaders(code, response.length());
+        if (contentType != null)
+            exh.getResponseHeaders().add("Content-Type", contentType +"; charset=" +Charset.defaultCharset().displayName());
+        // FIXME cache control
+        OutputStream os = exh.getResponseBody();
+        os.write(response.getBytes());
+        os.flush();
+        os.close();
+    }
+
+    private boolean FilterRequests(HttpExchange httpExchange) throws IOException {
+        if (httpExchange.getRequestMethod().equals("GET"))
+            return false;
+        SendResponse(httpExchange, 405, null, "Method Not Allowed");
+        return true;
+    }
+
+    private void ServeRoot(HttpExchange httpExchange) throws IOException {
+        if (FilterRequests(httpExchange))
+            return;
+        String os = "<!DOCTYPE html5>\n" +
+                "<html><body>\n" +
+                "<a href='/metrics'>/metrics</a>\n" +
+                "</body></html>";
+        SendResponse(httpExchange, 200, "text/html", os);
+    }
+
+    private void ServeMetrics(HttpExchange httpExchange) throws IOException {
+        if (FilterRequests(httpExchange))
+            return;
+        StringBuilder os = new StringBuilder();
+        for (CollectorValue i : CollectorManager.GetInstance()) {
+            os.append("# HELP ").append(i.fName).append(" ").append(i.fHelp).append("\n");
+            os.append("# TYPE ").append(i.fName).append(" ").append(i.fType.fName).append("\n");
+            os.append(i.fName).append(" ").append(i.toString()).append("\n");
+        }
+        SendResponse(httpExchange, 200, "text/plain", os.toString());
+    }
+}

+ 7 - 0
app/src/main/java/info/knacki/prometheusandroidexporter/ICollector.java

@@ -0,0 +1,7 @@
+package info.knacki.prometheusandroidexporter;
+
+import java.util.Collection;
+
+public interface ICollector {
+    Collection<CollectorValue> ReadValues();
+}

+ 18 - 0
app/src/main/java/info/knacki/prometheusandroidexporter/MainActivity.java

@@ -0,0 +1,18 @@
+package info.knacki.prometheusandroidexporter;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+public class MainActivity extends AppCompatActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        Intent i = new Intent(this, MainService.class);
+        startService(i);
+    }
+}

+ 48 - 0
app/src/main/java/info/knacki/prometheusandroidexporter/MainService.java

@@ -0,0 +1,48 @@
+package info.knacki.prometheusandroidexporter;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import androidx.annotation.Nullable;
+import androidx.work.PeriodicWorkRequest;
+import androidx.work.WorkManager;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import info.knacki.prometheusandroidexporter.collector.TestCollector;
+
+public class MainService extends Service {
+    public MainService() {
+    }
+
+    @Override
+    public void onCreate() {
+        InitCollectors(CollectorManager.GetInstance());
+        ScheduleCollection();
+        try {
+            HttpService.Register(getApplicationContext());
+        }
+        catch (IOException e) {
+            Logger.getLogger(MainService.class.getName()).log(Level.SEVERE, "Cannot start server: ", e);
+        }
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    private void InitCollectors(CollectorManager manager) {
+        manager.RegisterCollector(new TestCollector());
+    }
+
+    private void ScheduleCollection() {
+        PeriodicWorkRequest job = new PeriodicWorkRequest.Builder(CollectorWorker.class, 15, TimeUnit.MINUTES).build();
+        WorkManager.getInstance(this).enqueue(job);
+    }
+}

+ 23 - 0
app/src/main/java/info/knacki/prometheusandroidexporter/collector/TestCollector.java

@@ -0,0 +1,23 @@
+package info.knacki.prometheusandroidexporter.collector;
+
+import java.util.ArrayDeque;
+import java.util.Collection;
+
+import info.knacki.prometheusandroidexporter.CollectorValue;
+import info.knacki.prometheusandroidexporter.ICollector;
+
+public class TestCollector implements ICollector {
+    private final CollectorValue countCollector = new CollectorValue("counter", "A count that counts", CollectorValue.Type.COUNTER);
+    private int count =0;
+
+    @Override
+    public Collection<CollectorValue> ReadValues() {
+        ArrayDeque<CollectorValue> result = new ArrayDeque<>();
+
+        CollectorValue val = new CollectorValue(countCollector);
+        val.SetValue(count++);
+        result.add(val);
+
+        return result;
+    }
+}

+ 9 - 0
app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity">
+
+</androidx.constraintlayout.widget.ConstraintLayout>