From dcf458c4de7818a852e03415f2759ce4156e44f1 Mon Sep 17 00:00:00 2001 From: Jonathan Shook Date: Fri, 23 Oct 2020 02:35:41 -0500 Subject: [PATCH] annotation progress --- .../engine/clients/grafana/GrafanaClient.java | 362 ++++++++++++++++++ .../clients/grafana/transfer/Annotation.java | 169 ++++++++ .../engine/core/annotation/Annotators.java | 60 +++ .../core/metrics/GrafanaMetricsAnnotator.java | 102 +++++ .../nb/api/annotation/Annotator.java | 43 +++ 5 files changed, 736 insertions(+) create mode 100644 engine-clients/src/main/java/io/nosqlbench/engine/clients/grafana/GrafanaClient.java create mode 100644 engine-clients/src/main/java/io/nosqlbench/engine/clients/grafana/transfer/Annotation.java create mode 100644 engine-core/src/main/java/io/nosqlbench/engine/core/annotation/Annotators.java create mode 100644 engine-core/src/main/java/io/nosqlbench/engine/core/metrics/GrafanaMetricsAnnotator.java create mode 100644 nb-api/src/main/java/io/nosqlbench/nb/api/annotation/Annotator.java diff --git a/engine-clients/src/main/java/io/nosqlbench/engine/clients/grafana/GrafanaClient.java b/engine-clients/src/main/java/io/nosqlbench/engine/clients/grafana/GrafanaClient.java new file mode 100644 index 000000000..f70a3fd82 --- /dev/null +++ b/engine-clients/src/main/java/io/nosqlbench/engine/clients/grafana/GrafanaClient.java @@ -0,0 +1,362 @@ +package io.nosqlbench.engine.clients.grafana; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.nosqlbench.engine.clients.grafana.transfer.Annotation; +import io.nosqlbench.engine.clients.grafana.transfer.Annotations; + +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Base64; + +/** + * @see Grafana Annotations API Docs + */ +public class GrafanaClient { + + private final URI baseuri; + private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + Authenticator auth = null; + private String username; + private String password; + + public GrafanaClient(String baseurl) { + this.baseuri = initURI(baseurl); + } + + public void basicAuth(String username, String password) { + this.username = username; + this.password = password; + this.auth = new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password.toCharArray()); + } + }; + } + + private URI initURI(String baseurl) { + try { + URI uri = new URI(baseurl); + String userinfo = uri.getRawUserInfo(); + if (userinfo != null) { + String[] unpw = userinfo.split(":"); + this.username = unpw[0]; + this.password = unpw[1]; + uri = new URI(baseurl.replace(userinfo + "@", "")); + } + return uri; + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + private HttpClient getClient() { + HttpClient.Builder cb = HttpClient.newBuilder(); + if (this.auth != null) { + cb.authenticator(auth); + } + HttpClient client = cb.build(); + return client; + } + + private URI makeUri(String pathAndQuery) { + try { + return new URI(this.baseuri.toString() + pathAndQuery); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + /** + *
{@code
+     * GET /api/annotations?from=1506676478816&to=1507281278816&tags=tag1&tags=tag2&limit=100
+     *
+     * Example Request:
+     *
+     * GET /api/annotations?from=1506676478816&to=1507281278816&tags=tag1&tags=tag2&limit=100 HTTP/1.1
+     * Accept: application/json
+     * Content-Type: application/json
+     * Authorization: Basic YWRtaW46YWRtaW4=
+     * Query Parameters:
+     *
+     * from: epoch datetime in milliseconds. Optional.
+     * to: epoch datetime in milliseconds. Optional.
+     * limit: number. Optional - default is 100. Max limit for results returned.
+     * alertId: number. Optional. Find annotations for a specified alert.
+     * dashboardId: number. Optional. Find annotations that are scoped to a specific dashboard
+     * panelId: number. Optional. Find annotations that are scoped to a specific panel
+     * userId: number. Optional. Find annotations created by a specific user
+     * type: string. Optional. alert|annotation Return alerts or user created annotations
+     * tags: string. Optional. Use this to filter global annotations. Global annotations are annotations from an annotation data source that are not connected specifically to a dashboard or panel. To do an “AND” filtering with multiple tags, specify the tags parameter multiple times e.g. tags=tag1&tags=tag2.
+     * Example Response:
+     *
+     * HTTP/1.1 200
+     * Content-Type: application/json
+     * [
+     *     {
+     *         "id": 1124,
+     *         "alertId": 0,
+     *         "dashboardId": 468,
+     *         "panelId": 2,
+     *         "userId": 1,
+     *         "userName": "",
+     *         "newState": "",
+     *         "prevState": "",
+     *         "time": 1507266395000,
+     *         "timeEnd": 1507266395000,
+     *         "text": "test",
+     *         "metric": "",
+     *         "type": "event",
+     *         "tags": [
+     *             "tag1",
+     *             "tag2"
+     *         ],
+     *         "data": {}
+     *     },
+     *     {
+     *         "id": 1123,
+     *         "alertId": 0,
+     *         "dashboardId": 468,
+     *         "panelId": 2,
+     *         "userId": 1,
+     *         "userName": "",
+     *         "newState": "",
+     *         "prevState": "",
+     *         "time": 1507265111000,
+     *         "text": "test",
+     *         "metric": "",
+     *         "type": "event",
+     *         "tags": [
+     *             "tag1",
+     *             "tag2"
+     *         ],
+     *         "data": {}
+     *     }
+     * ]
+     * }
+ * + * @param by + * @return + */ + public Annotations findAnnotations(By... by) { + + String query = By.fields(by); + HttpRequest.Builder rqb = HttpRequest.newBuilder(makeUri("api/annotations?" + query)); + rqb = addAuth(rqb); + rqb.setHeader("Content-Type", "application/json"); + HttpRequest request = rqb.build(); + + HttpClient client = getClient(); + HttpResponse response = null; + + try { + response = client.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (Exception e) { + throw new RuntimeException(e); + } + String body = response.body(); + Annotations annotations = gson.fromJson(body, Annotations.class); + return annotations; + } + + /** + *
{@code
+     * POST /api/annotations
+     *
+     * Example Request:
+     *
+     * POST /api/annotations HTTP/1.1
+     * Accept: application/json
+     * Content-Type: application/json
+     *
+     * {
+     *   "dashboardId":468,
+     *   "panelId":1,
+     *   "time":1507037197339,
+     *   "timeEnd":1507180805056,
+     *   "tags":["tag1","tag2"],
+     *   "text":"Annotation Description"
+     * }
+     * Example Response:
+     *
+     * HTTP/1.1 200
+     * Content-Type: application/json
+     *
+     * {
+     *     "message":"Annotation added",
+     *     "id": 1,
+     * }
+     * }
+ * + * @return + */ + public Annotation createAnnotation(Annotation annotation) { + HttpClient client = getClient(); + HttpRequest.Builder rqb = HttpRequest.newBuilder(makeUri("api/annotations")); + rqb = addAuth(rqb); + rqb.setHeader("Content-Type", "application/json"); + String rqBody = gson.toJson(annotation); + rqb = rqb.POST(HttpRequest.BodyPublishers.ofString(rqBody)); + addAuth(rqb); + + HttpResponse response = null; + try { + response = client.send(rqb.build(), HttpResponse.BodyHandlers.ofString()); + } catch (Exception e) { + if (e.getMessage().contains("WWW-Authenticate header missing")) { + throw new RuntimeException("Java HttpClient was not authorized, and it saw no WWW-Authenticate header" + + " in the response, so this is probably Grafana telling you that the auth scheme failed. Normally " + + "this error would be thrown by Java HttpClient:" + e.getMessage()); + } + throw new RuntimeException(e); + } + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new RuntimeException("Creating annotation failed with status code " + response.statusCode() + " at " + + "baseurl " + baseuri + ": " + response.body()); + } + String body = response.body(); + Annotation savedAnnotation = gson.fromJson(body, Annotation.class); + return savedAnnotation; + } + + private HttpRequest.Builder addAuth(HttpRequest.Builder rqb) { + if (this.username != null && this.password != null) { + rqb = rqb.setHeader("Authorization", encodeBasicAuth(username, password)); + } + return rqb; + } + + private static String encodeBasicAuth(String username, String password) { + return "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); + } + + /** + *
{@code
+     * POST /api/annotations/graphite
+     *
+     * Example Request:
+     *
+     * POST /api/annotations/graphite HTTP/1.1
+     * Accept: application/json
+     * Content-Type: application/json
+     *
+     * {
+     *   "what": "Event - deploy",
+     *   "tags": ["deploy", "production"],
+     *   "when": 1467844481,
+     *   "data": "deploy of master branch happened at Wed Jul 6 22:34:41 UTC 2016"
+     * }
+     * Example Response:
+     *
+     * HTTP/1.1 200
+     * Content-Type: application/json
+     *
+     * {
+     *     "message":"Graphite annotation added",
+     *     "id": 1
+     * }
+     * }
+ * + * @return + */ + public Annotation createGraphiteAnnotation() { + return null; + } + + /** + *
{@code
+     * PUT /api/annotations/:id
+     *
+     * Updates all properties of an annotation that matches the specified id. To only update certain property, consider using the Patch Annotation operation.
+     *
+     * Example Request:
+     *
+     * PUT /api/annotations/1141 HTTP/1.1
+     * Accept: application/json
+     * Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+     * Content-Type: application/json
+     *
+     * {
+     *   "time":1507037197339,
+     *   "timeEnd":1507180805056,
+     *   "text":"Annotation Description",
+     *   "tags":["tag3","tag4","tag5"]
+     * }
+     * Example Response:
+     *
+     * HTTP/1.1 200
+     * Content-Type: application/json
+     *
+     * {
+     *     "message":"Annotation updated"
+     * }
+     * }
+ */ + public void updateAnnotation() { + + } + + /** + *
{@code
+     * PATCH /api/annotations/:id
+     *
+     * Updates one or more properties of an annotation that matches the specified id.
+     *
+     * This operation currently supports updating of the text, tags, time and timeEnd properties.
+     *
+     * Example Request:
+     *
+     * PATCH /api/annotations/1145 HTTP/1.1
+     * Accept: application/json
+     * Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+     * Content-Type: application/json
+     *
+     * {
+     *   "text":"New Annotation Description",
+     *   "tags":["tag6","tag7","tag8"]
+     * }
+     * Example Response:
+     *
+     * HTTP/1.1 200
+     * Content-Type: application/json
+     *
+     * {
+     *     "message":"Annotation patched"
+     * }
+     * }
+ */ + public void patchAnnotation() { + + } + + /** + *
{@code
+     * Example Request:
+     *
+     * DELETE /api/annotations/1 HTTP/1.1
+     * Accept: application/json
+     * Content-Type: application/json
+     * Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+     * Example Response:
+     *
+     * HTTP/1.1 200
+     * Content-Type: application/json
+     *
+     * {
+     *     "message":"Annotation deleted"
+     * }
+     * }
+ * + * @param id + */ + public void deleteAnnotation(long id) { + + } + +} diff --git a/engine-clients/src/main/java/io/nosqlbench/engine/clients/grafana/transfer/Annotation.java b/engine-clients/src/main/java/io/nosqlbench/engine/clients/grafana/transfer/Annotation.java new file mode 100644 index 000000000..125a1acf6 --- /dev/null +++ b/engine-clients/src/main/java/io/nosqlbench/engine/clients/grafana/transfer/Annotation.java @@ -0,0 +1,169 @@ +package io.nosqlbench.engine.clients.grafana.transfer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Annotation { + + private Integer id; + private Integer alertId; + private Integer dashboardId; + private Integer panelId; + private Integer userId; + private String userName; + private String newState; + private String prevState; + private Long time; + private Long timeEnd; + private String text; + private String metric; + private String type; + private List tags = new ArrayList(); + private Object data; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getAlertId() { + return alertId; + } + + public void setAlertId(Integer alertId) { + this.alertId = alertId; + } + + public Integer getDashboardId() { + return dashboardId; + } + + public void setDashboardId(Integer dashboardId) { + this.dashboardId = dashboardId; + } + + public Integer getPanelId() { + return panelId; + } + + public void setPanelId(Integer panelId) { + this.panelId = panelId; + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getNewState() { + return newState; + } + + public void setNewState(String newState) { + this.newState = newState; + } + + public String getPrevState() { + return prevState; + } + + public void setPrevState(String prevState) { + this.prevState = prevState; + } + + public Long getTime() { + return time; + } + + public void setTime(Long time) { + this.time = time; + } + + public Long getTimeEnd() { + return timeEnd; + } + + public void setTimeEnd(Long timeEnd) { + this.timeEnd = timeEnd; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getMetric() { + return metric; + } + + public void setMetric(String metric) { + this.metric = metric; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public void setTags(String tags) { + this.tags = Arrays.asList(tags.split("\\\\s,\\\\s")); + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + @Override + public String toString() { + return "Annotation{" + + "id=" + id + + ", alertId=" + alertId + + ", dashboardId=" + dashboardId + + ", panelId=" + panelId + + ", userId=" + userId + + ", userName='" + userName + '\'' + + ", newState='" + newState + '\'' + + ", prevState='" + prevState + '\'' + + ", time=" + time + + ", timeEnd=" + timeEnd + + ", text='" + text + '\'' + + ", metric='" + metric + '\'' + + ", type='" + type + '\'' + + ", tags=" + tags + + ", data=" + data + + '}'; + } +} diff --git a/engine-core/src/main/java/io/nosqlbench/engine/core/annotation/Annotators.java b/engine-core/src/main/java/io/nosqlbench/engine/core/annotation/Annotators.java new file mode 100644 index 000000000..d0af040ae --- /dev/null +++ b/engine-core/src/main/java/io/nosqlbench/engine/core/annotation/Annotators.java @@ -0,0 +1,60 @@ +package io.nosqlbench.engine.core.annotation; + +import io.nosqlbench.nb.api.annotation.Annotator; + +import java.util.*; + +public class Annotators { + + private static List annotators; + private static Set names; + + /** + * Initialize the active annotators. + * + * @param annotatorsConfig A comma-separated set of annotator configs, each with optional + * configuration metadata in name{config} form. + */ + public synchronized static void init(String annotatorsConfig) { + if (annotatorsConfig == null || annotatorsConfig.isEmpty()) { + Annotators.names = Set.of(); + } else { + + } + Annotators.names = names; + } + + public synchronized static List getAnnotators() { + if (names == null) { + throw new RuntimeException("Annotators.init(...) must be called first."); + } + if (annotators == null) { + annotators = new ArrayList<>(); + ServiceLoader loader = ServiceLoader.load(Annotator.class); + loader.stream() + .map(sp -> sp.get()) + .filter(an -> names.contains("all") || Annotators.names.contains(an.getName())) + .forEach(an -> { + annotators.add(an); + }); + } + return annotators; + } + + public static synchronized void recordAnnotation( + String sessionName, + long startEpochMillis, + long endEpochMillis, + Map target, + Map details) { + getAnnotators().forEach(a -> a.recordAnnotation(sessionName, startEpochMillis, endEpochMillis, target, details)); + } + + public static synchronized void recordAnnotation( + String sessionName, + Map target, + Map details) { + recordAnnotation(sessionName, 0L, 0L, target, details); + } + +} diff --git a/engine-core/src/main/java/io/nosqlbench/engine/core/metrics/GrafanaMetricsAnnotator.java b/engine-core/src/main/java/io/nosqlbench/engine/core/metrics/GrafanaMetricsAnnotator.java new file mode 100644 index 000000000..8e558b794 --- /dev/null +++ b/engine-core/src/main/java/io/nosqlbench/engine/core/metrics/GrafanaMetricsAnnotator.java @@ -0,0 +1,102 @@ +package io.nosqlbench.engine.core.metrics; + +import io.nosqlbench.engine.clients.grafana.GrafanaClient; +import io.nosqlbench.engine.clients.grafana.transfer.Annotation; +import io.nosqlbench.nb.api.annotation.Annotator; +import io.nosqlbench.nb.api.config.ConfigAware; +import io.nosqlbench.nb.api.config.ConfigModel; +import io.nosqlbench.nb.api.config.MutableConfigModel; + +import java.util.Map; +import java.util.Optional; + +public class GrafanaMetricsAnnotator implements Annotator, ConfigAware { + + private final GrafanaClient client; + + public GrafanaMetricsAnnotator(String grafanaBaseUrl) { + this.client = new GrafanaClient(grafanaBaseUrl); + } + + + @Override + public void recordAnnotation(String sessionName, long startEpochMillis, long endEpochMillis, Map target, Map details) { + + Annotation annotation = new Annotation(); + + // Target + + Optional.ofNullable(target.get("type")) + .ifPresent(annotation::setType); + + long startAt = startEpochMillis > 0 ? startEpochMillis : System.currentTimeMillis(); + annotation.setTime(startAt); + annotation.setTimeEnd(endEpochMillis > 0 ? endEpochMillis : startAt); + + String eTime = target.get("timeEnd"); + annotation.setTimeEnd((eTime != null) ? Long.valueOf(eTime) : null); + + Optional.ofNullable(target.get("id")).map(Integer::valueOf) + .ifPresent(annotation::setId); + + Optional.ofNullable(target.get("alertId")).map(Integer::valueOf) + .ifPresent(annotation::setAlertId); + + Optional.ofNullable(target.get("dashboardId")).map(Integer::valueOf) + .ifPresent(annotation::setDashboardId); + + Optional.ofNullable(target.get("panelId")).map(Integer::valueOf) + .ifPresent(annotation::setPanelId); + + Optional.ofNullable(target.get("userId")).map(Integer::valueOf) + .ifPresent(annotation::setUserId); + + Optional.ofNullable(target.get("userName")) + .ifPresent(annotation::setUserName); + + Optional.ofNullable(target.get("tags")) + .ifPresent(annotation::setTags); + + Optional.ofNullable(details.get("metric")) + .ifPresent(annotation::setMetric); + + // Details + + StringBuilder sb = new StringBuilder(); + if (details.containsKey("text")) { + annotation.setText(details.get("text")); + } else { + for (String dkey : details.keySet()) { + sb.append(sb).append(": ").append(details.get(dkey)).append("\n"); + } + annotation.setText(details.toString()); + } + + Optional.ofNullable(details.get("data")) + .ifPresent(annotation::setData); + + Optional.ofNullable(details.get("prevState")) + .ifPresent(annotation::setPrevState); + Optional.ofNullable(details.get("newState")) + .ifPresent(annotation::setNewState); + + Annotation created = this.client.createAnnotation(annotation); + } + + @Override + public String getName() { + return "grafana"; + } + + @Override + public void applyConfig(Map element) { + ConfigModel configModel = getConfigModel(); + + + } + + @Override + public ConfigModel getConfigModel() { + return new MutableConfigModel().add("baseurl", String.class).asReadOnly(); + } +} diff --git a/nb-api/src/main/java/io/nosqlbench/nb/api/annotation/Annotator.java b/nb-api/src/main/java/io/nosqlbench/nb/api/annotation/Annotator.java new file mode 100644 index 000000000..ad5ba272c --- /dev/null +++ b/nb-api/src/main/java/io/nosqlbench/nb/api/annotation/Annotator.java @@ -0,0 +1,43 @@ +package io.nosqlbench.nb.api.annotation; + +import io.nosqlbench.nb.spi.Named; + +import java.util.Map; + +/** + * An implementation of this type is responsible for taking annotation details and + * logging them in a useful place. + */ +public interface Annotator extends Named { + + /** + * Submit an annotation to some type of annotation store or logging or eventing mechanism. + * Implementations of this service are responsible for mapping the scenarioName, target, + * and details into the native schema of the target annotation or logging system in whichever + * way would be the least surprising for a user. + * + * The target is the nominative data which identifies the identity of the annotation. This + * must include enough information to allow the annotation to be homed and located within + * a target system such that is is visible where it should be seen. This includes all + * metadata which may be used to filter or locate the annotation, including timestamps. + * + * The details contain payload information to be displayed within the body of the annotation. + * + * @param sessionName The name of the scenario + * @param startEpochMillis The epoch millisecond instant of the annotation, set this to 0 to have it + * automatically set to the current system time. + * @param endEpochMillis The epoch millisecond instant at the end of the interval. If this is + * equal to the start instant, then this is an annotation for a point in time. + * This will be the default behavior if this value is 0. + * @param target The target of the annotation, fields which are required to associate the + * annotation with the correct instance of a dashboard, metrics, etc + * @param details A map of details + */ + void recordAnnotation( + String sessionName, + long startEpochMillis, + long endEpochMillis, + Map target, + Map details); + +}