Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use actuator/metrics for data points #615

Merged
merged 33 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
74a0718
Can we use actuator
clemens-tolboom Jan 24, 2024
6e2fa10
Merge remote-tracking branch 'origin/master' into feat/metrics
clemens-tolboom Jan 25, 2024
c161c88
Restructure little
clemens-tolboom Jan 25, 2024
1ec80ae
Merge remote-tracking branch 'origin/master' into feat/metrics
clemens-tolboom Jan 31, 2024
ea557fb
Display each metric
clemens-tolboom Jan 31, 2024
b7123ed
Make it more condensed
clemens-tolboom Feb 1, 2024
33af39e
Add FileMetrics example
clemens-tolboom Feb 1, 2024
ef5854b
Merge remote-tracking branch 'origin/master' into feat/metrics
clemens-tolboom Feb 9, 2024
22ef955
Use button group
clemens-tolboom Feb 9, 2024
dfc3a8c
Small fixes
clemens-tolboom Feb 12, 2024
3676460
feat: add metrics with search
clemens-tolboom Feb 12, 2024
ac512fc
cleanup and dev edgecases
clemens-tolboom Feb 13, 2024
ec1214b
dev: ignore other data/project
clemens-tolboom Feb 13, 2024
774a567
Add other actuator links at bottom.
clemens-tolboom Feb 13, 2024
87bd688
Fix link by binding
clemens-tolboom Feb 13, 2024
38f6483
Add types for used /actuator request.
clemens-tolboom Feb 14, 2024
0aa0fc1
codestyle: remove soms TS warnings
clemens-tolboom Feb 26, 2024
e398e96
add spinner while loading
clemens-tolboom Feb 26, 2024
b56b8a7
Cleanup search words
clemens-tolboom Feb 26, 2024
71c1c87
Fix loader status
clemens-tolboom Feb 26, 2024
fcba6bf
Code style.
clemens-tolboom Feb 26, 2024
ae16ac1
More cleanup
clemens-tolboom Feb 26, 2024
65705c2
Add objectDeepCopy and use it
clemens-tolboom Feb 26, 2024
f1fdd5d
Add objectDeepCopy and use it
clemens-tolboom Feb 26, 2024
2ad41a4
Move template up
clemens-tolboom Feb 26, 2024
66943f7
Move template up
clemens-tolboom Feb 26, 2024
0c5914e
Use const
clemens-tolboom Feb 26, 2024
37c6f10
Move metric doc up
clemens-tolboom Feb 26, 2024
efa0d5c
Fix failing build and cleanup
clemens-tolboom Mar 7, 2024
f4979d3
Fix failing build and cleanup
clemens-tolboom Mar 7, 2024
a7a9b23
Merge remote-tracking branch 'origin/master' into feat/metrics
clemens-tolboom Mar 7, 2024
c992d6a
Cleanup code from feedback
clemens-tolboom Mar 13, 2024
b6f996e
Merge remote-tracking branch 'origin/master' into feat/metrics
clemens-tolboom Mar 13, 2024
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ dist
### STORAGE ###
data/user-*
data/system
# Ignore all projects but life-cycle
data/shared-*
!data/shared-lifecycle

HELP.md
target/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.molgenis.armadillo.info;

import io.micrometer.common.lang.NonNull;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.MeterBinder;
import java.io.File;
import org.springframework.stereotype.Component;

@Component
public class FileMetrics implements MeterBinder {
clemens-tolboom marked this conversation as resolved.
Show resolved Hide resolved

private static final String PATH = System.getProperty("user.dir");

@Override
public void bindTo(@NonNull MeterRegistry registry) {
File folder = new File(PATH);
File[] listOfFiles = folder.listFiles();

int fileCount = 0;
int dirCount = 0;

if (listOfFiles != null) {
for (File file : listOfFiles) {
if (file.isFile()) {
fileCount++;
} else if (file.isDirectory()) {
dirCount++;
}
}
}

Gauge.builder("user.files.count", fileCount, Integer::doubleValue)
.description("Number of files in the current directory")
.baseUnit("files")
.tags("path", PATH)
.register(registry);

Gauge.builder("user.directories.count", dirCount, Integer::doubleValue)
.description("Number of directories in the current directory")
.baseUnit("directories")
.tags("path", PATH)
.register(registry);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
public class RMetrics {

@Bean
MeterBinder rProcesses(ProfileService profileService, RProcessEndpoint processes) {
MeterBinder rProcesses(ProfileService profileService, RProcessEndpoint rProcessEndpoint) {

return registry ->
runAsSystem(
Expand All @@ -24,7 +24,7 @@ MeterBinder rProcesses(ProfileService profileService, RProcessEndpoint processes
environment ->
Gauge.builder(
"rserve.processes.current",
() -> processes.countRServeProcesses(environment))
() -> rProcessEndpoint.countRServeProcesses(environment))
.tag("environment", environment)
.description(
"Current number of RServe processes on the R environment")
Expand Down
61 changes: 59 additions & 2 deletions ui/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiError } from "@/helpers/errors";
import { sanitizeObject } from "@/helpers/utils";
import { objectDeepCopy, sanitizeObject } from "@/helpers/utils";
import {
Principal,
Profile,
Expand All @@ -8,7 +8,11 @@ import {
Auth,
RemoteFileInfo,
RemoteFileDetail,
Metric,
HalResponse,
Metrics,
} from "@/types/api";

import { ObjectWithStringKey, StringArray } from "@/types/types";
import { APISettings } from "./config";

Expand Down Expand Up @@ -88,7 +92,7 @@ export async function handleResponse(response: Response) {
}
}

export async function getActuator() {
export async function getActuator(): Promise<HalResponse> {
let result = await get("/actuator");
return result;
}
Expand All @@ -103,6 +107,59 @@ export async function getVersion() {
return result.build.version;
}

/**
* Fetch all metric values on one go using the list.
*/
export async function getMetricsAll(): Promise<Metrics> {
const paths = await getMetrics();
const metrics: Metrics = {};

await Promise.all(
paths.map(async (path) => {
const response = await getMetric(path);
metrics[path] = response;
return { path: response };
})
);

return metrics;
}

/**
* Get list of metric IDs in as dictionary keys.
*/
async function getMetrics(): Promise<string[]> {
const path = "/actuator/metrics";
return await get(path)
.then((data) => {
// Check if the data has 'names' property
if (data.hasOwnProperty("names")) {
clemens-tolboom marked this conversation as resolved.
Show resolved Hide resolved
return data.names;
} else {
console.log("No names found in the data");
clemens-tolboom marked this conversation as resolved.
Show resolved Hide resolved
return [];
}
})
.catch((error) => {
console.error(`Error fetching ${path}`, error);
clemens-tolboom marked this conversation as resolved.
Show resolved Hide resolved
return {};
});
}

/**
* Fetches give Metric ID.
*
* @path: dot separated string
*
* Example: a.b.c
*/
async function getMetric(id: string): Promise<Metric> {
const path = `/actuator/metrics/${id}`;
return get(path).then((data) => {
return objectDeepCopy<Metric>(data);
});
}

export async function deleteUser(email: string) {
return delete_("/access/users", email);
}
Expand Down
175 changes: 175 additions & 0 deletions ui/src/components/Actuator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<template>
<div class="row">
<div class="col mt-3" v-if="isLoading">
<LoadingSpinner />
clemens-tolboom marked this conversation as resolved.
Show resolved Hide resolved
</div>
<div class="col" v-else>
<div class="row">
<div class="col-sm-3">
<SearchBar id="searchbox" v-model="filterValue" />
</div>
<div class="col">
<button
class="btn btn-primary float-end"
v-if="metrics"
@click="downloadMetrics"
>
<i class="bi bi-box-arrow-down"></i>
Download metrics
</button>
</div>
</div>
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th>key</th>
<th>statistic</th>
<th>value</th>
</tr>
</thead>
<tbody>
<ActuatorItem
v-for="(metric, path, index) in metrics"
:key="index"
:data="metric"
:name="path"
/>
</tbody>
</table>
<hr />
<summary>
<h3>Other Actuator links</h3>
<details>
<table>
<thead>
<tr>
<td>key</td>
<td>href</td>
<td>templated</td>
</tr>
</thead>
<tbody>
<tr v-for="(item, key) in actuator" :key="key">
<td>{{ key }}</td>
<td v-if="item.templated">{{ item.href }}</td>
<td v-if="!item.templated">
<a :href="item.href" target="_new">{{ item.href }}</a>
</td>
<td>{{ item.templated }}</td>
</tr>
</tbody>
</table>
</details>
</summary>
</div>
</div>
</template>

<script setup lang="ts">
clemens-tolboom marked this conversation as resolved.
Show resolved Hide resolved
import { getActuator, getMetricsAll } from "@/api/api";
import { ref, watch } from "vue";
import { Metrics, HalLinks } from "@/types/api";
import { ObjectWithStringKey } from "@/types/types";
import { objectDeepCopy } from "@/helpers/utils";
import ActuatorItem from "./ActuatorItem.vue";
import SearchBar from "@/components/SearchBar.vue";
import LoadingSpinner from "./LoadingSpinner.vue";
const actuator = ref<HalLinks>();
const metrics = ref<Metrics>([]);
const isLoading = ref<boolean>(true);
const loadActuator = async () => {
let result = (await getActuator())["_links"];
let list = [];
for (let key in result) {
clemens-tolboom marked this conversation as resolved.
Show resolved Hide resolved
// Add key to each item for further usage
const item = result[key];
item["key"] = key;
list.push(result[key]);
clemens-tolboom marked this conversation as resolved.
Show resolved Hide resolved
}
actuator.value = list;
};
const loadMetrics = async () => {
metrics.value = await getMetricsAll();
// preload search values
filteredLines();
isLoading.value = false;
};
loadMetrics();
loadActuator();
Copy link
Collaborator

Choose a reason for hiding this comment

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

See how I did the loading in my branch. That way your UI won't freeze during loading and show a loading spinner:

function emitIfLoadingDone() {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Let's discuss

function downloadJSON(filename: string) {
const cleanedUp = removeFields(metrics.value);
const dataStr =
"data:text/json;charset=utf-8," +
encodeURIComponent(JSON.stringify(cleanedUp));
const downloadAnchorNode = document.createElement("a");
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", filename + ".json");
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
setTimeout(() => downloadAnchorNode.remove(), 10);
}
function downloadMetrics() {
downloadJSON("armadillo-metrics-" + new Date().toISOString());
}
const filterValue = ref("");
watch(filterValue, (_newVal, _oldVal) => filteredLines());
const FIELD_DISPLAY = "_display";
const SEARCH_TEXT_FIELDS = "searchWords";
function concatValues(obj: any): string {
let result = "";
for (const key in obj) {
if (typeof obj[key] === "object" && obj[key] !== null) {
result += concatValues(obj[key]);
} else {
result += obj[key];
}
}
return result;
}
/**
* Filter metrics on search value
*
* We add:
* - string search field for matching
* - booelan display field for storing matched
*/
function filteredLines() {
clemens-tolboom marked this conversation as resolved.
Show resolved Hide resolved
const filterOn: string = filterValue.value.toLowerCase();
for (let [_key, value] of Object.entries(metrics.value)) {
if (!value[SEARCH_TEXT_FIELDS]) {
value[SEARCH_TEXT_FIELDS] = concatValues(value).toLowerCase();
}
const searchWords: string = value[SEARCH_TEXT_FIELDS];
value[FIELD_DISPLAY] = filterOn === "" || searchWords.includes(filterOn);
clemens-tolboom marked this conversation as resolved.
Show resolved Hide resolved
}
}
/**
* Remove added fields for searching.
*
* @param json
*/
function removeFields(json: Metrics) {
const result: Metrics = objectDeepCopy<Metrics>(json);
for (let [_key, value] of Object.entries(result)) {
const wrapper: ObjectWithStringKey = value;
delete wrapper[SEARCH_TEXT_FIELDS];
delete wrapper[FIELD_DISPLAY];
}
return result;
}
</script>
56 changes: 56 additions & 0 deletions ui/src/components/ActuatorItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<template>
<tr v-if="data._display" v-for="(v, key) in data.measurements" :key="key">
<td scope="col">{{ key }}</td>
<td :title="data.description">
<span>
{{ data.name }}
<i v-if="data.description" class="bi bi-info-circle-fill"></i>
</span>
</td>
<td>{{ v.statistic }}</td>
<td v-if="data.baseUnit === 'bytes'">
{{ convertBytes(v.value) }}
</td>
<td v-else>{{ v.value }} {{ data.baseUnit }}</td>
</tr>
<tr v-if="data._display">
<td colspan="5">
<summary>
<details>
<pre>
{{ JSON.stringify(data, null, 3) }}
</pre>
</details>
</summary>
</td>
</tr>
</template>

<script setup lang="ts">
const props = defineProps({
name: {
type: String,
required: true,
},
data: {
type: Object,
required: true,
},
});
/**
* Convert given bytes to 2 digits precision round exponent version string.
* @param bytes number
*/
function convertBytes(bytes: number): string {
clemens-tolboom marked this conversation as resolved.
Show resolved Hide resolved
const units = ["bytes", "KB", "MB", "GB", "TB", "EB"];
let unitIndex = 0;
while (bytes >= 1024 && unitIndex < units.length - 1) {
bytes /= 1024;
unitIndex++;
}
return `${bytes.toFixed(2)} ${units[unitIndex]}`;
}
</script>
Loading