allow users with admin:metrics to read app metrics

This commit is contained in:
FloatingGhost 2022-12-16 03:32:51 +00:00
parent b8be8192fb
commit c2054f82ab
14 changed files with 283 additions and 44 deletions

1
.gitignore vendored
View file

@ -76,3 +76,4 @@ docs/site
# docker stuff # docker stuff
docker-db docker-db
*.iml

View file

@ -3,9 +3,13 @@ defmodule Pleroma.Web.AkkomaAPI.MetricsController do
alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.OAuthScopesPlug
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []} plug(
plug(:skip_auth) OAuthScopesPlug,
%{scopes: ["admin:metrics"]}
when action in [
:show
]
)
def show(conn, _params) do def show(conn, _params) do
stats = TelemetryMetricsPrometheus.Core.scrape() stats = TelemetryMetricsPrometheus.Core.scrape()

View file

@ -197,7 +197,11 @@ defp incorporate_conn_info(action_settings, %{params: params} = conn) do
}) })
end end
defp ip(%{remote_ip: remote_ip}) do defp ip(%{remote_ip: remote_ip}) when is_binary(remote_ip) do
remote_ip
end
defp ip(%{remote_ip: remote_ip}) when is_tuple(remote_ip) do
remote_ip remote_ip
|> Tuple.to_list() |> Tuple.to_list()
|> Enum.join(".") |> Enum.join(".")

View file

@ -868,7 +868,11 @@ defmodule Pleroma.Web.Router do
scope "/" do scope "/" do
pipe_through([:pleroma_html, :authenticate, :require_admin]) pipe_through([:pleroma_html, :authenticate, :require_admin])
live_dashboard("/phoenix/live_dashboard", metrics: {Pleroma.Web.Telemetry, :live_dashboard_metrics}, csp_nonce_assign_key: :csp_nonce)
live_dashboard("/phoenix/live_dashboard",
metrics: {Pleroma.Web.Telemetry, :live_dashboard_metrics},
csp_nonce_assign_key: :csp_nonce
)
end end
# Test-only routes needed to test action dispatching and plug chain execution # Test-only routes needed to test action dispatching and plug chain execution
@ -907,6 +911,7 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web.Fallback do scope "/", Pleroma.Web.Fallback do
get("/registration/:token", RedirectController, :registration_page) get("/registration/:token", RedirectController, :registration_page)
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta) get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
get("/api/*path", RedirectController, :api_not_implemented)
get("/*path", RedirectController, :redirector_with_preload) get("/*path", RedirectController, :redirector_with_preload)
options("/*path", RedirectController, :empty) options("/*path", RedirectController, :empty)

View file

@ -11,16 +11,13 @@ def start_link(arg) do
def init(_arg) do def init(_arg) do
children = [ children = [
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}, {:telemetry_poller, measurements: periodic_measurements(), period: 10_000},
{TelemetryMetricsPrometheus, metrics: prometheus_metrics(), plug_cowboy_opts: [ip: {127, 0, 0, 1}]} {TelemetryMetricsPrometheus.Core, metrics: prometheus_metrics()}
] ]
Supervisor.init(children, strategy: :one_for_one) Supervisor.init(children, strategy: :one_for_one)
end end
@doc """ # A seperate set of metrics for distributions because phoenix dashboard does NOT handle them well
A seperate set of metrics for distributions because phoenix dashboard does NOT handle
them well
"""
defp distribution_metrics do defp distribution_metrics do
[ [
distribution( distribution(
@ -110,8 +107,6 @@ defp summary_metrics do
summary("vm.total_run_queue_lengths.total"), summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"), summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io"), summary("vm.total_run_queue_lengths.io"),
last_value("pleroma.local_users.total"), last_value("pleroma.local_users.total"),
last_value("pleroma.domains.total"), last_value("pleroma.domains.total"),
last_value("pleroma.local_statuses.total") last_value("pleroma.local_statuses.total")

View file

@ -4,17 +4,33 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui"> <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
<title><%= Pleroma.Config.get([:instance, :name]) %></title> <title><%= Pleroma.Config.get([:instance, :name]) %></title>
<link rel="stylesheet" href="/instance/static.css"> <link rel="stylesheet" href="/static-fe/static-fe.css">
<link rel="stylesheet" href="/static-fe/forms.css">
</head> </head>
<body> <body>
<div class="instance-header">
<a class="instance-header__content" href="/"> <div class="background-image"></div>
<img class="instance-header__thumbnail" src="<%= Pleroma.Config.get([:instance, :instance_thumbnail]) %>"> <nav>
<h1 class="instance-header__title"><%= Pleroma.Config.get([:instance, :name]) %></h1> <div class="inner-nav">
<a class="site-brand" href="/">
<img class="favicon" src="/favicon.png" />
<span><%= Pleroma.Config.get([:instance, :name]) %></span>
</a> </a>
</div> </div>
</nav>
<div class="container"> <div class="container">
<div class="underlay"></div>
<div class="column main flex">
<div class="panel oauth">
<%= @inner_content %> <%= @inner_content %>
</div> </div>
</div>
</div>
</body> </body>
<style>
:root {
--background-image: url("<%= Pleroma.Config.get([:instance, :background_image]) %>");
}
</style>
</html> </html>

View file

@ -1,2 +1,8 @@
<h1><%= Gettext.dpgettext("static_pages", "oauth authorized page title", "Successfully authorized") %></h1> <div>
<h2><%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@auth.token))) %></h2> <div class="panel-heading">
<%= Gettext.dpgettext("static_pages", "oauth authorized page title", "Successfully authorized") %>
</div>
<div class="panel-content">
<%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@auth.token))) %>
</div>
</div>

View file

@ -20,7 +20,9 @@
<div class="container__content"> <div class="container__content">
<%= if @app do %> <%= if @app do %>
<div class="panel-heading">
<p><%= raw Gettext.dpgettext("static_pages", "oauth authorize message", "Application <strong>%{client_name}</strong> is requesting access to your account.", client_name: safe_to_string(html_escape(@app.client_name))) %></p> <p><%= raw Gettext.dpgettext("static_pages", "oauth authorize message", "Application <strong>%{client_name}</strong> is requesting access to your account.", client_name: safe_to_string(html_escape(@app.client_name))) %></p>
</div>
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
<% end %> <% end %>

View file

@ -163,7 +163,7 @@ defp deps do
{:telemetry, "~> 0.3"}, {:telemetry, "~> 0.3"},
{:telemetry_poller, "~> 0.4"}, {:telemetry_poller, "~> 0.4"},
{:telemetry_metrics, "~> 0.4"}, {:telemetry_metrics, "~> 0.4"},
{:telemetry_metrics_prometheus, "~> 1.1.0"}, {:telemetry_metrics_prometheus_core, "~> 1.1.0"},
{:poolboy, "~> 1.5"}, {:poolboy, "~> 1.5"},
{:recon, "~> 2.5"}, {:recon, "~> 2.5"},
{:joken, "~> 2.0"}, {:joken, "~> 2.0"},

View file

@ -0,0 +1,114 @@
form {
width: 100%;
}
.input {
color: var(--muted-text-color);
display: flex;
margin-left: 1em;
margin-right: 1em;
flex-direction: column;
}
input {
padding: 10px;
margin-top: 5px;
margin-bottom: 10px;
background-color: var(--background-color);
color: var(--primary-text-color);
border: 0;
transition-property: border-bottom;
transition-duration: 0.35s;
border-bottom: 2px solid #2a384a;
font-size: 14px;
width: inherit;
box-sizing: border-box;
}
.scopes-input {
display: flex;
flex-direction: column;
margin: 1em 0;
color: var(--muted-text-color);
}
.scopes-input label:first-child {
height: 2em;
}
.scopes {
display: flex;
flex-wrap: wrap;
color: var(--primary-text-color);
}
.scope {
display: flex;
flex-basis: 100%;
height: 2em;
align-items: center;
}
.scope:before {
color: var(--primary-text-color);
content: "✔\fe0e";
margin-left: 1em;
margin-right: 1em;
}
[type="checkbox"]+label {
display: none;
cursor: pointer;
margin: 0.5em;
}
[type="checkbox"] {
display: none;
}
[type="checkbox"]+label:before {
cursor: pointer;
display: inline-block;
color: white;
background-color: var(--background-color);
border: 4px solid var(--background-color);
box-shadow: 0px 0px 1px 0 var(--brand-color);
width: 1.2em;
height: 1.2em;
margin-right: 1.0em;
content: "";
transition-property: background-color;
transition-duration: 0.35s;
color: var(--background-color);
margin-bottom: -0.2em;
border-radius: 2px;
}
[type="checkbox"]:checked+label:before {
background-color: var(--brand-color);
}
a.button,
button {
width: 100%;
background-color: #1c2a3a;
color: var(--primary-text-color);
border-radius: 4px;
border: none;
padding: 10px 16px;
margin-top: 20px;
margin-bottom: 20px;
text-transform: uppercase;
font-size: 16px;
box-shadow: 0px 0px 2px 0px black,
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
}
a.button:hover,
button:hover {
cursor: pointer;
box-shadow: 0px 0px 0px 1px var(--brand-color),
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
}

View file

@ -28,9 +28,11 @@ :root {
--border: rgba(26, 37, 53, 1); --border: rgba(26, 37, 53, 1);
--poll: rgba(99, 84, 72, 1); --poll: rgba(99, 84, 72, 1);
} }
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root { :root {
--icon-filter: invert(67%) sepia(7%) saturate(525%) hue-rotate(173deg) brightness(90%) contrast(92%);; --icon-filter: invert(67%) sepia(7%) saturate(525%) hue-rotate(173deg) brightness(90%) contrast(92%);
;
--wallpaper: rgba(248, 250, 252, 1); --wallpaper: rgba(248, 250, 252, 1);
--alertNeutral: rgba(48, 64, 85, 0.5); --alertNeutral: rgba(48, 64, 85, 0.5);
--alertNeutralText: rgba(0, 0, 0, 1); --alertNeutralText: rgba(0, 0, 0, 1);
@ -155,6 +157,10 @@ .panel-heading {
box-shadow: var(--panelHeaderShadow); box-shadow: var(--panelHeaderShadow);
} }
.panel-content {
padding: 1em;
}
.about-content { .about-content {
padding: 0.6em; padding: 0.6em;
} }
@ -169,6 +175,18 @@ .sidebar {
padding-left: 0.5em; padding-left: 0.5em;
} }
.column.flex {
grid-column-end: sidebar-end;
}
.scopes-input {
display: flex;
flex-direction: column;
margin: 1em 0;
color: var(--muted-text-color);
}
.status-container, .status-container,
.repeat-header, .repeat-header,
.user-card { .user-card {
@ -193,6 +211,7 @@ .repeat-header {
.repeat-header .right-side { .repeat-header .right-side {
color: var(--faint); color: var(--faint);
} }
.repeat-header .u-photo { .repeat-header .u-photo {
height: 20px; height: 20px;
width: 20px; width: 20px;
@ -255,6 +274,7 @@ .heading-reply-row {
.reply-to-link { .reply-to-link {
color: var(--faint); color: var(--faint);
} }
.reply-to-link:hover { .reply-to-link:hover {
text-decoration: underline; text-decoration: underline;
} }
@ -280,11 +300,13 @@ .h-card {
margin-bottom: 8px; margin-bottom: 8px;
} }
header a, .h-card a { header a,
.h-card a {
text-decoration: none; text-decoration: none;
} }
header a:hover, .h-card a:hover { header a:hover,
.h-card a:hover {
text-decoration: underline; text-decoration: underline;
} }
@ -322,6 +344,7 @@ .nsfw-banner {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.nsfw-banner div { .nsfw-banner div {
width: 100%; width: 100%;
text-align: center; text-align: center;
@ -330,6 +353,7 @@ .nsfw-banner div {
.nsfw-banner:not(:hover) { .nsfw-banner:not(:hover) {
background-color: var(--background); background-color: var(--background);
} }
.nsfw-banner:hover div { .nsfw-banner:hover div {
display: none; display: none;
} }
@ -342,10 +366,12 @@ .poll-option {
word-break: break-word; word-break: break-word;
z-index: 1; z-index: 1;
} }
.poll-option .percentage { .poll-option .percentage {
width: 3.5em; width: 3.5em;
flex-shrink: 0; flex-shrink: 0;
} }
.poll-option .fill { .poll-option .fill {
height: 100%; height: 100%;
position: absolute; position: absolute;
@ -362,6 +388,7 @@ .status-actions {
display: flex; display: flex;
margin-top: 0.75em; margin-top: 0.75em;
} }
.status-actions>* { .status-actions>* {
max-width: 4em; max-width: 4em;
flex: 1; flex: 1;

65
scripts/create_metrics_app.sh Executable file
View file

@ -0,0 +1,65 @@
#!/bin/sh
read -p "Instance URL (e.g https://example.com): " INSTANCE_URL
echo "Creating oauth app..."
RESP=$(curl \
-XPOST \
$INSTANCE_URL/api/v1/apps \
--silent \
--data-urlencode 'client_name=fedibash' \
--data-urlencode 'redirect_uris=urn:ietf:wg:oauth:2.0:oob' \
--data-urlencode 'scopes=admin:metrics' \
--header "Content-Type: application/x-www-form-urlencoded"
)
client_id=$(echo $RESP | jq -r .client_id)
client_secret=$(echo $RESP | jq -r .client_secret)
if [ -z "$client_id"]; then
echo "Could not create an app"
echo "$RESP"
exit 1
fi
echo "Please visit the following URL and input the code provided"
AUTH_URL="$INSTANCE_URL/oauth/authorize?client_id=$client_id&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=admin:metrics&response_type=code"
if [ ! -z "$BROWSER" ]; then
$BROWSER $AUTH_URL
fi;
echo $AUTH_URL
read -p "Code: " CODE
echo "Requesting code..."
RESP=$(curl \
-XPOST \
$INSTANCE_URL/oauth/token \
--silent \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=$client_id" \
--data-urlencode "client_secret=$client_secret" \
--data-urlencode "code=$CODE" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode 'redirect_uri=urn:ietf:wg:oauth:2.0:oob' \
--data-urlencode "scope=admin:metrics"
)
ACCESS_TOKEN="$(echo $RESP | jq -r .access_token)"
echo "Token is $ACCESS_TOKEN"
DOMAIN=$(echo $INSTANCE_URL | sed -e 's/^https:\/\///')
echo "Use the following config in your prometheus.yml:
- job_name: akkoma
scheme: https
authorization:
credentials: $ACCESS_TOKEN
metrics_path: /api/v1/akkoma/metrics
static_configs:
- targets:
- $DOMAIN
"

View file

@ -554,7 +554,7 @@ def oauth_app_factory do
%Pleroma.Web.OAuth.App{ %Pleroma.Web.OAuth.App{
client_name: sequence(:client_name, &"Some client #{&1}"), client_name: sequence(:client_name, &"Some client #{&1}"),
redirect_uris: "https://example.com/callback", redirect_uris: "https://example.com/callback",
scopes: ["read", "write", "follow", "push", "admin"], scopes: ["read", "write", "follow", "push"],
website: "https://example.com", website: "https://example.com",
client_id: Ecto.UUID.generate(), client_id: Ecto.UUID.generate(),
client_secret: "aaa;/&bbb" client_secret: "aaa;/&bbb"