Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
LOGFLARE_NODE_HOST=127.0.0.1
|
||||
DB_USERNAME=supabase_admin
|
||||
DB_DATABASE=_supabase
|
||||
DB_HOSTNAME=supabase-db
|
||||
DB_PORT=5432
|
||||
DB_PASSWORD=${secret:postgres}
|
||||
DB_SCHEMA=_analytics
|
||||
LOGFLARE_API_KEY=${secret:logflare}
|
||||
LOGFLARE_SINGLE_TENANT=true
|
||||
LOGFLARE_SUPABASE_MODE=true
|
||||
LOGFLARE_MIN_CLUSTER_SIZE=1
|
||||
POSTGRES_BACKEND_URL=postgresql://supabase_admin:${secret:postgres}@supabase-db:5432/_supabase
|
||||
POSTGRES_BACKEND_SCHEMA=_analytics
|
||||
LOGFLARE_FEATURE_FLAG_OVERRIDE=multibackend=true
|
||||
@@ -0,0 +1,86 @@
|
||||
# Generated by the mesh. Do not edit: module supabase writes this file and replaces it at every push.
|
||||
#
|
||||
# logflare 1.4.0 copies two of its settings into its own database once and never reads them again:
|
||||
# LOGFLARE_API_KEY becomes the default user's api_key when that user is created, and
|
||||
# POSTGRES_BACKEND_URL becomes each source's backend when that source is created. A changed value
|
||||
# reaches neither: a rotated key is refused, and a moved database is never written to (every
|
||||
# source here pointed at db:5432, a name that resolves nowhere, so nothing was stored).
|
||||
#
|
||||
# Run by the start script before logflare starts, through logflare's own code (its Repo, its Users
|
||||
# and Backends contexts and their changesets): it makes both copies say what the environment says.
|
||||
# It changes nothing when they already do, says what it changed, never prints the key or a URL's
|
||||
# password, and stops the start when it cannot finish.
|
||||
fail = fn why ->
|
||||
IO.puts(:stderr, "analytics: #{why}; not starting")
|
||||
System.halt(1)
|
||||
end
|
||||
|
||||
say = fn what -> IO.puts(:stderr, "analytics: #{what}") end
|
||||
|
||||
key = System.get_env("LOGFLARE_API_KEY")
|
||||
url = System.get_env("POSTGRES_BACKEND_URL")
|
||||
schema = System.get_env("POSTGRES_BACKEND_SCHEMA")
|
||||
|
||||
if key in [nil, ""], do: fail.("LOGFLARE_API_KEY is not set, so the key logflare must accept is unknown")
|
||||
if url in [nil, ""], do: fail.("POSTGRES_BACKEND_URL is not set, so where logflare must store logs is unknown")
|
||||
|
||||
where = fn u ->
|
||||
case URI.parse(to_string(u)) do
|
||||
%URI{host: host, port: port, path: path} when is_binary(host) -> "#{host}:#{port}#{path}"
|
||||
_ -> "an unreadable URL"
|
||||
end
|
||||
end
|
||||
|
||||
Application.ensure_all_started(:ssl)
|
||||
|
||||
outcome =
|
||||
Ecto.Migrator.with_repo(Logflare.Repo, fn _repo ->
|
||||
# Backends.update_source_backend_config restarts the source's pipeline after saving, and looks
|
||||
# it up in this registry to do so; logflare is not running yet, so the registry is empty and
|
||||
# there is nothing to restart.
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: Logflare.Backends.SourceRegistry)
|
||||
|
||||
case Logflare.SingleTenant.get_default_user() do
|
||||
nil ->
|
||||
say.("no default user yet; logflare creates it from LOGFLARE_API_KEY and POSTGRES_BACKEND_URL as it starts")
|
||||
:ok
|
||||
|
||||
user ->
|
||||
if user.api_key == key and is_nil(user.old_api_key) do
|
||||
say.("the default user's API key is LOGFLARE_API_KEY already")
|
||||
else
|
||||
# old_api_key is cleared too: logflare's UI can swap it back in, and the key replaced
|
||||
# here may be one that leaked.
|
||||
case Logflare.Users.update_user_all_fields(user, %{api_key: key, old_api_key: nil}) do
|
||||
{:ok, _} -> say.("the default user's API key was replaced with LOGFLARE_API_KEY")
|
||||
{:error, changeset} -> fail.("the default user's API key could not be replaced: #{inspect(changeset.errors)}")
|
||||
end
|
||||
end
|
||||
|
||||
results =
|
||||
for source <- Logflare.Sources.list_sources_by_user(user),
|
||||
backend <- Logflare.Backends.list_source_backends(source),
|
||||
backend.type == :postgres do
|
||||
if backend.config.url == url and backend.config.schema == schema do
|
||||
:same
|
||||
else
|
||||
case Logflare.Backends.update_source_backend_config(backend, %{url: url, schema: schema}) do
|
||||
{:ok, _} ->
|
||||
say.("source #{source.name} stored logs at #{where.(backend.config.url)} schema #{inspect(backend.config.schema)}; now at #{where.(url)} schema #{inspect(schema)}")
|
||||
|
||||
{:error, changeset} ->
|
||||
fail.("source #{source.name}'s backend could not be pointed at #{where.(url)}: #{inspect(changeset.errors)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
say.("#{Enum.count(results, &(&1 == :same))} source backend(s) already at #{where.(url)}")
|
||||
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
|
||||
case outcome do
|
||||
{:ok, :ok, _} -> :ok
|
||||
other -> fail.("logflare's database could not be reconciled: #{inspect(other)}")
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
# Generated by the mesh. Do not edit: module supabase writes this file and replaces it at every push.
|
||||
#
|
||||
# Two things logflare 1.4.0 does not do for itself, done here before it starts; either failing
|
||||
# stops the start, with the reason as the last line of this container's log.
|
||||
#
|
||||
# 1. The API key never travels in a URL. logflare prints the whole URL of every request that fails,
|
||||
# query string included, in the error report Plug.Cowboy writes ("Request: POST
|
||||
# /api/logs?source_name=...&api_key=..."). That is an error, so no log level hides it while errors
|
||||
# are logged at all (novox/hq issue 268). logflare reads the key from the x-api-key header as well,
|
||||
# and the mesh's caller, vector, sends it there; this refuses to start logflare while the vector
|
||||
# config it is given still passes the key in a URL: an analytics that does not start says why
|
||||
# here, and one that leaks says nothing.
|
||||
#
|
||||
# 2. Its database says what its environment says. logflare copies LOGFLARE_API_KEY into its default
|
||||
# user and POSTGRES_BACKEND_URL into every source's backend once, when it creates them, and never
|
||||
# again; reconcile.exs makes both copies match the environment through logflare's own code, after
|
||||
# its migrations and before it starts. That is what lets the mesh rotate the key by starting
|
||||
# logflare again, and what moves the sources' backend when the database's address or password
|
||||
# changes.
|
||||
set -e
|
||||
conf=/run/logflare/vector.yml
|
||||
if [ ! -r "$conf" ]; then
|
||||
echo "analytics: $conf is not there to check, so whether the API key travels in a URL is unknown; not starting (novox/hq issue 268)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -v '^[[:space:]]*#' "$conf" | grep -q 'api_key='; then
|
||||
echo "analytics: $conf passes the API key in a URL, and logflare prints a failed request's URL; not starting (novox/hq issue 268)" >&2
|
||||
exit 1
|
||||
fi
|
||||
reconcile=/run/logflare/reconcile.exs
|
||||
if [ ! -r "$reconcile" ]; then
|
||||
echo "analytics: $reconcile is not there, so whether logflare's stored key and backends match its environment is unknown; not starting" >&2
|
||||
exit 1
|
||||
fi
|
||||
cd /opt/app/rel/logflare/bin
|
||||
# The tables reconcile.exs reads exist only once the migrations have run; run.sh runs them again,
|
||||
# which finds nothing left to do.
|
||||
./logflare eval Logflare.Release.migrate
|
||||
./logflare eval 'Code.eval_file("/run/logflare/reconcile.exs")'
|
||||
exec sh run.sh
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER DATABASE postgres SET "app.settings.jwt_secret" TO '${secret:jwt}';
|
||||
ALTER DATABASE postgres SET "app.settings.jwt_exp" TO '3600';
|
||||
@@ -0,0 +1,6 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
\c _supabase
|
||||
create schema if not exists _analytics;
|
||||
alter schema _analytics owner to :pguser;
|
||||
\c postgres
|
||||
@@ -0,0 +1,6 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
\c _supabase
|
||||
create schema if not exists _supavisor;
|
||||
alter schema _supavisor owner to :pguser;
|
||||
\c postgres
|
||||
@@ -0,0 +1,4 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
create schema if not exists _realtime;
|
||||
alter schema _realtime owner to :pguser;
|
||||
@@ -0,0 +1,7 @@
|
||||
\set pgpass `echo "$POSTGRES_PASSWORD"`
|
||||
|
||||
ALTER USER authenticator WITH PASSWORD :'pgpass';
|
||||
ALTER USER pgbouncer WITH PASSWORD :'pgpass';
|
||||
ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass';
|
||||
ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass';
|
||||
ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass';
|
||||
@@ -0,0 +1,3 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
CREATE DATABASE _supabase WITH OWNER :pguser;
|
||||
@@ -0,0 +1,208 @@
|
||||
BEGIN;
|
||||
-- Create pg_net extension
|
||||
CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions;
|
||||
-- Create supabase_functions schema
|
||||
CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin;
|
||||
GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role;
|
||||
-- supabase_functions.migrations definition
|
||||
CREATE TABLE supabase_functions.migrations (
|
||||
version text PRIMARY KEY,
|
||||
inserted_at timestamptz NOT NULL DEFAULT NOW()
|
||||
);
|
||||
-- Initial supabase_functions migration
|
||||
INSERT INTO supabase_functions.migrations (version) VALUES ('initial');
|
||||
-- supabase_functions.hooks definition
|
||||
CREATE TABLE supabase_functions.hooks (
|
||||
id bigserial PRIMARY KEY,
|
||||
hook_table_id integer NOT NULL,
|
||||
hook_name text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT NOW(),
|
||||
request_id bigint
|
||||
);
|
||||
CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id);
|
||||
CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name);
|
||||
COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.';
|
||||
CREATE FUNCTION supabase_functions.http_request()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
request_id bigint;
|
||||
payload jsonb;
|
||||
url text := TG_ARGV[0]::text;
|
||||
method text := TG_ARGV[1]::text;
|
||||
headers jsonb DEFAULT '{}'::jsonb;
|
||||
params jsonb DEFAULT '{}'::jsonb;
|
||||
timeout_ms integer DEFAULT 1000;
|
||||
BEGIN
|
||||
IF url IS NULL OR url = 'null' THEN
|
||||
RAISE EXCEPTION 'url argument is missing';
|
||||
END IF;
|
||||
|
||||
IF method IS NULL OR method = 'null' THEN
|
||||
RAISE EXCEPTION 'method argument is missing';
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN
|
||||
headers = '{"Content-Type": "application/json"}'::jsonb;
|
||||
ELSE
|
||||
headers = TG_ARGV[2]::jsonb;
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN
|
||||
params = '{}'::jsonb;
|
||||
ELSE
|
||||
params = TG_ARGV[3]::jsonb;
|
||||
END IF;
|
||||
|
||||
IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN
|
||||
timeout_ms = 1000;
|
||||
ELSE
|
||||
timeout_ms = TG_ARGV[4]::integer;
|
||||
END IF;
|
||||
|
||||
CASE
|
||||
WHEN method = 'GET' THEN
|
||||
SELECT http_get INTO request_id FROM net.http_get(
|
||||
url,
|
||||
params,
|
||||
headers,
|
||||
timeout_ms
|
||||
);
|
||||
WHEN method = 'POST' THEN
|
||||
payload = jsonb_build_object(
|
||||
'old_record', OLD,
|
||||
'record', NEW,
|
||||
'type', TG_OP,
|
||||
'table', TG_TABLE_NAME,
|
||||
'schema', TG_TABLE_SCHEMA
|
||||
);
|
||||
|
||||
SELECT http_post INTO request_id FROM net.http_post(
|
||||
url,
|
||||
payload,
|
||||
params,
|
||||
headers,
|
||||
timeout_ms
|
||||
);
|
||||
ELSE
|
||||
RAISE EXCEPTION 'method argument % is invalid', method;
|
||||
END CASE;
|
||||
|
||||
INSERT INTO supabase_functions.hooks
|
||||
(hook_table_id, hook_name, request_id)
|
||||
VALUES
|
||||
(TG_RELID, TG_NAME, request_id);
|
||||
|
||||
RETURN NEW;
|
||||
END
|
||||
$function$;
|
||||
-- Supabase super admin
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_roles
|
||||
WHERE rolname = 'supabase_functions_admin'
|
||||
)
|
||||
THEN
|
||||
CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin;
|
||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin;
|
||||
ALTER USER supabase_functions_admin SET search_path = "supabase_functions";
|
||||
ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin;
|
||||
ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin;
|
||||
ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin;
|
||||
GRANT supabase_functions_admin TO postgres;
|
||||
-- Remove unused supabase_pg_net_admin role
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_roles
|
||||
WHERE rolname = 'supabase_pg_net_admin'
|
||||
)
|
||||
THEN
|
||||
REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin;
|
||||
DROP OWNED BY supabase_pg_net_admin;
|
||||
DROP ROLE supabase_pg_net_admin;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
-- pg_net grants when extension is already enabled
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_extension
|
||||
WHERE extname = 'pg_net'
|
||||
)
|
||||
THEN
|
||||
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
-- Event trigger for pg_net
|
||||
CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access()
|
||||
RETURNS event_trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_event_trigger_ddl_commands() AS ev
|
||||
JOIN pg_extension AS ext
|
||||
ON ev.objid = ext.oid
|
||||
WHERE ext.extname = 'pg_net'
|
||||
)
|
||||
THEN
|
||||
GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER;
|
||||
ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net;
|
||||
REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net';
|
||||
DO
|
||||
$$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_event_trigger
|
||||
WHERE evtname = 'issue_pg_net_access'
|
||||
) THEN
|
||||
CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION')
|
||||
EXECUTE PROCEDURE extensions.grant_pg_net_access();
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants');
|
||||
ALTER function supabase_functions.http_request() SECURITY DEFINER;
|
||||
ALTER function supabase_functions.http_request() SET search_path = supabase_functions;
|
||||
REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
JWT_SECRET=${secret:jwt}
|
||||
SUPABASE_URL=http://supabase-kong:8000
|
||||
SUPABASE_ANON_KEY=${secret:anon-key}
|
||||
SUPABASE_SERVICE_ROLE_KEY=${secret:service-role-key}
|
||||
SUPABASE_DB_URL=postgresql://postgres:${secret:postgres}@supabase-db:5432/postgres
|
||||
VERIFY_JWT=false
|
||||
@@ -0,0 +1,94 @@
|
||||
import { serve } from 'https://deno.land/std@0.131.0/http/server.ts'
|
||||
import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts'
|
||||
|
||||
console.log('main function started')
|
||||
|
||||
const JWT_SECRET = Deno.env.get('JWT_SECRET')
|
||||
const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true'
|
||||
|
||||
function getAuthToken(req: Request) {
|
||||
const authHeader = req.headers.get('authorization')
|
||||
if (!authHeader) {
|
||||
throw new Error('Missing authorization header')
|
||||
}
|
||||
const [bearer, token] = authHeader.split(' ')
|
||||
if (bearer !== 'Bearer') {
|
||||
throw new Error(`Auth header is not 'Bearer {token}'`)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
async function verifyJWT(jwt: string): Promise<boolean> {
|
||||
const encoder = new TextEncoder()
|
||||
const secretKey = encoder.encode(JWT_SECRET)
|
||||
try {
|
||||
await jose.jwtVerify(jwt, secretKey)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
serve(async (req: Request) => {
|
||||
if (req.method !== 'OPTIONS' && VERIFY_JWT) {
|
||||
try {
|
||||
const token = getAuthToken(req)
|
||||
const isValidJWT = await verifyJWT(token)
|
||||
|
||||
if (!isValidJWT) {
|
||||
return new Response(JSON.stringify({ msg: 'Invalid JWT' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return new Response(JSON.stringify({ msg: e.toString() }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const url = new URL(req.url)
|
||||
const { pathname } = url
|
||||
const path_parts = pathname.split('/')
|
||||
const service_name = path_parts[1]
|
||||
|
||||
if (!service_name || service_name === '') {
|
||||
const error = { msg: 'missing function name in request' }
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const servicePath = `/home/deno/functions/${service_name}`
|
||||
console.error(`serving the request with ${servicePath}`)
|
||||
|
||||
const memoryLimitMb = 150
|
||||
const workerTimeoutMs = 1 * 60 * 1000
|
||||
const noModuleCache = false
|
||||
const importMapPath = null
|
||||
const envVarsObj = Deno.env.toObject()
|
||||
const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]])
|
||||
|
||||
try {
|
||||
const worker = await EdgeRuntime.userWorkers.create({
|
||||
servicePath,
|
||||
memoryLimitMb,
|
||||
workerTimeoutMs,
|
||||
noModuleCache,
|
||||
importMapPath,
|
||||
envVars,
|
||||
})
|
||||
return await worker.fetch(req)
|
||||
} catch (e) {
|
||||
const error = { msg: e.toString() }
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
GOTRUE_API_HOST=0.0.0.0
|
||||
GOTRUE_API_PORT=9999
|
||||
API_EXTERNAL_URL=https://${bound:route:name}
|
||||
GOTRUE_DB_DRIVER=postgres
|
||||
GOTRUE_DB_DATABASE_URL=postgres://supabase_auth_admin:${secret:postgres}@supabase-db:5432/postgres
|
||||
GOTRUE_SITE_URL=https://${bound:route:name}
|
||||
GOTRUE_URI_ALLOW_LIST=
|
||||
GOTRUE_DISABLE_SIGNUP=true
|
||||
GOTRUE_JWT_ADMIN_ROLES=service_role
|
||||
GOTRUE_JWT_AUD=authenticated
|
||||
GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated
|
||||
GOTRUE_JWT_EXP=3600
|
||||
GOTRUE_JWT_SECRET=${secret:jwt}
|
||||
GOTRUE_EXTERNAL_EMAIL_ENABLED=false
|
||||
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false
|
||||
GOTRUE_MAILER_AUTOCONFIRM=false
|
||||
GOTRUE_SMTP_ADMIN_EMAIL=admin@example.com
|
||||
GOTRUE_SMTP_HOST=supabase-mail
|
||||
GOTRUE_SMTP_PORT=2500
|
||||
GOTRUE_SMTP_USER=fake_mail_user
|
||||
GOTRUE_SMTP_PASS=fake_mail_password
|
||||
GOTRUE_SMTP_SENDER_NAME=fake_sender
|
||||
GOTRUE_MAILER_URLPATHS_INVITE=/auth/v1/verify
|
||||
GOTRUE_MAILER_URLPATHS_CONFIRMATION=/auth/v1/verify
|
||||
GOTRUE_MAILER_URLPATHS_RECOVERY=/auth/v1/verify
|
||||
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=/auth/v1/verify
|
||||
GOTRUE_EXTERNAL_PHONE_ENABLED=false
|
||||
GOTRUE_SMS_AUTOCONFIRM=true
|
||||
@@ -0,0 +1,242 @@
|
||||
# Written by the mesh (modules/supabase): the gateway's declarative config, credentials rendered in.
|
||||
_format_version: '2.1'
|
||||
_transform: true
|
||||
|
||||
###
|
||||
### Consumers / Users
|
||||
###
|
||||
consumers:
|
||||
- username: DASHBOARD
|
||||
- username: anon
|
||||
keyauth_credentials:
|
||||
- key: "${secret:anon-key}"
|
||||
- username: service_role
|
||||
keyauth_credentials:
|
||||
- key: "${secret:service-role-key}"
|
||||
|
||||
###
|
||||
### Access Control List
|
||||
###
|
||||
acls:
|
||||
- consumer: anon
|
||||
group: anon
|
||||
- consumer: service_role
|
||||
group: admin
|
||||
|
||||
###
|
||||
### Dashboard credentials
|
||||
###
|
||||
basicauth_credentials:
|
||||
- consumer: DASHBOARD
|
||||
username: "${secret:dashboard-user}"
|
||||
password: "${secret:dashboard}"
|
||||
|
||||
###
|
||||
### API Routes
|
||||
###
|
||||
services:
|
||||
## Open Auth routes
|
||||
- name: auth-v1-open
|
||||
url: http://supabase-auth:9999/verify
|
||||
routes:
|
||||
- name: auth-v1-open
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/verify
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-callback
|
||||
url: http://supabase-auth:9999/callback
|
||||
routes:
|
||||
- name: auth-v1-open-callback
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/callback
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-authorize
|
||||
url: http://supabase-auth:9999/authorize
|
||||
routes:
|
||||
- name: auth-v1-open-authorize
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/authorize
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## Secure Auth routes
|
||||
- name: auth-v1
|
||||
_comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*'
|
||||
url: http://supabase-auth:9999/
|
||||
routes:
|
||||
- name: auth-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure REST routes
|
||||
- name: rest-v1
|
||||
_comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'
|
||||
url: http://supabase-rest:3000/
|
||||
routes:
|
||||
- name: rest-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /rest/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure GraphQL routes
|
||||
- name: graphql-v1
|
||||
_comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql'
|
||||
url: http://supabase-rest:3000/rpc/graphql
|
||||
routes:
|
||||
- name: graphql-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /graphql/v1
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- Content-Profile:graphql_public
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure Realtime routes
|
||||
- name: realtime-v1-ws
|
||||
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
|
||||
url: http://realtime-dev.supabase-realtime:4000/socket
|
||||
protocol: ws
|
||||
routes:
|
||||
- name: realtime-v1-ws
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
- name: realtime-v1-rest
|
||||
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
|
||||
url: http://realtime-dev.supabase-realtime:4000/api
|
||||
protocol: http
|
||||
routes:
|
||||
- name: realtime-v1-rest
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/api
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
## Storage routes: the storage server manages its own auth
|
||||
- name: storage-v1
|
||||
_comment: 'Storage: /storage/v1/* -> http://storage:5000/*'
|
||||
url: http://supabase-storage:5000/
|
||||
routes:
|
||||
- name: storage-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /storage/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## Edge Functions routes
|
||||
- name: functions-v1
|
||||
_comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*'
|
||||
url: http://supabase-edge-functions:9000/
|
||||
routes:
|
||||
- name: functions-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /functions/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## Analytics routes
|
||||
- name: analytics-v1
|
||||
_comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*'
|
||||
url: http://supabase-analytics:4000/
|
||||
routes:
|
||||
- name: analytics-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /analytics/v1/
|
||||
|
||||
## Secure Database routes
|
||||
- name: meta
|
||||
_comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*'
|
||||
url: http://supabase-meta:8080/
|
||||
routes:
|
||||
- name: meta-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /pg/
|
||||
plugins:
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
|
||||
## Protected Dashboard - catch all remaining routes
|
||||
- name: dashboard
|
||||
_comment: 'Studio: /* -> http://studio:3000/*'
|
||||
url: http://supabase-studio:3000/
|
||||
routes:
|
||||
- name: dashboard-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: basic-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
@@ -0,0 +1,6 @@
|
||||
PG_META_PORT=8080
|
||||
PG_META_DB_HOST=supabase-db
|
||||
PG_META_DB_PORT=5432
|
||||
PG_META_DB_NAME=postgres
|
||||
PG_META_DB_USER=supabase_admin
|
||||
PG_META_DB_PASSWORD=${secret:postgres}
|
||||
@@ -0,0 +1,14 @@
|
||||
PORT=4000
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=postgres
|
||||
POSTGRES_PASSWORD=${secret:postgres}
|
||||
DATABASE_URL=ecto://supabase_admin:${secret:postgres}@supabase-db:5432/_supabase
|
||||
CLUSTER_POSTGRES=true
|
||||
SECRET_KEY_BASE=${secret:key-base-a}${secret:key-base-b}
|
||||
VAULT_ENC_KEY=${secret:pooler-vault}
|
||||
API_JWT_SECRET=${secret:jwt}
|
||||
METRICS_JWT_SECRET=${secret:jwt}
|
||||
REGION=local
|
||||
ERL_AFLAGS=-proto_dist inet_tcp
|
||||
RELEASE_NODE=supavisor@127.0.0.1
|
||||
POOLER_POOL_MODE=transaction
|
||||
@@ -0,0 +1,43 @@
|
||||
{:ok, _} = Application.ensure_all_started(:supavisor)
|
||||
|
||||
# Written by the mesh (modules/supabase). Per-machine values come from settings.json (the module's
|
||||
# one merge:json file); secrets come from the environment the image reads.
|
||||
settings = "/etc/pooler/settings.json" |> File.read!() |> Jason.decode!() |> Map.get("pooler", %{})
|
||||
|
||||
{:ok, version} =
|
||||
case Supavisor.Repo.query!("select version()") do
|
||||
%{rows: [[ver]]} -> Supavisor.Helpers.parse_pg_version(ver)
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
params = %{
|
||||
"external_id" => to_string(settings["tenant"] || "default"),
|
||||
"db_host" => "supabase-db",
|
||||
"db_port" => System.get_env("POSTGRES_PORT"),
|
||||
"db_database" => System.get_env("POSTGRES_DB"),
|
||||
"require_user" => false,
|
||||
"auth_query" => "SELECT * FROM pgbouncer.get_auth($1)",
|
||||
"default_max_clients" => settings["max-client-connections"] || 100,
|
||||
"default_pool_size" => settings["pool-size"] || 20,
|
||||
"default_parameter_status" => %{"server_version" => version},
|
||||
"users" => [%{
|
||||
"db_user" => "pgbouncer",
|
||||
"db_password" => System.get_env("POSTGRES_PASSWORD"),
|
||||
"mode_type" => System.get_env("POOLER_POOL_MODE"),
|
||||
"pool_size" => settings["pool-size"] || 20,
|
||||
"is_manager" => true
|
||||
}]
|
||||
}
|
||||
|
||||
case Supavisor.Tenants.get_tenant_by_external_id(params["external_id"]) do
|
||||
nil ->
|
||||
{:ok, _} = Supavisor.Tenants.create_tenant(params)
|
||||
|
||||
# A tenant made elsewhere (HAL named the database host "db") is pointed at this module's
|
||||
# database container; nothing else it holds is changed.
|
||||
%{db_host: host} = tenant when host != "supabase-db" ->
|
||||
{:ok, _} = Supavisor.Tenants.update_tenant(tenant, %{"db_host" => "supabase-db"})
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
db-uri = "postgres://authenticator:${secret:postgres}@supabase-db:5432/postgres"
|
||||
db-schemas = "public,storage,graphql_public"
|
||||
db-anon-role = "anon"
|
||||
jwt-secret = "${secret:jwt}"
|
||||
db-use-legacy-gucs = false
|
||||
app.settings.jwt_secret = "${secret:jwt}"
|
||||
app.settings.jwt_exp = "3600"
|
||||
@@ -0,0 +1,16 @@
|
||||
PORT=4000
|
||||
DB_HOST=supabase-db
|
||||
DB_PORT=5432
|
||||
DB_USER=supabase_admin
|
||||
DB_PASSWORD=${secret:postgres}
|
||||
DB_NAME=postgres
|
||||
DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime
|
||||
DB_ENC_KEY=supabaserealtime
|
||||
API_JWT_SECRET=${secret:jwt}
|
||||
SECRET_KEY_BASE=${secret:key-base-a}${secret:key-base-b}
|
||||
ERL_AFLAGS=-proto_dist inet_tcp
|
||||
DNS_NODES=''
|
||||
RLIMIT_NOFILE=10000
|
||||
APP_NAME=realtime
|
||||
SEED_SELF_HOST=true
|
||||
RUN_JANITOR=true
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"pooler": {
|
||||
"tenant": "default",
|
||||
"pool-size": 20,
|
||||
"max-client-connections": 100
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
ANON_KEY=${secret:anon-key}
|
||||
SERVICE_KEY=${secret:service-role-key}
|
||||
POSTGREST_URL=http://supabase-rest:3000
|
||||
PGRST_JWT_SECRET=${secret:jwt}
|
||||
DATABASE_URL=postgres://supabase_storage_admin:${secret:postgres}@supabase-db:5432/postgres
|
||||
FILE_SIZE_LIMIT=52428800
|
||||
STORAGE_BACKEND=file
|
||||
FILE_STORAGE_BACKEND_PATH=/var/lib/storage
|
||||
TENANT_ID=stub
|
||||
REGION=stub
|
||||
GLOBAL_S3_BUCKET=stub
|
||||
ENABLE_IMAGE_TRANSFORMATION=true
|
||||
IMGPROXY_URL=http://supabase-imgproxy:5001
|
||||
@@ -0,0 +1,18 @@
|
||||
# Next.js listens on the address HOSTNAME names, and docker sets HOSTNAME to the container's id,
|
||||
# so studio answered only on its network address while its image's health check asks localhost:
|
||||
# unhealthy for ever while working. Every address, as the upstream compose file has it.
|
||||
HOSTNAME=::
|
||||
STUDIO_PG_META_URL=http://supabase-meta:8080
|
||||
POSTGRES_PASSWORD=${secret:postgres}
|
||||
DEFAULT_ORGANIZATION_NAME=Default Organization
|
||||
DEFAULT_PROJECT_NAME=Default Project
|
||||
OPENAI_API_KEY=${secret:openai}
|
||||
SUPABASE_URL=http://supabase-kong:8000
|
||||
SUPABASE_PUBLIC_URL=https://${bound:route:name}
|
||||
SUPABASE_ANON_KEY=${secret:anon-key}
|
||||
SUPABASE_SERVICE_KEY=${secret:service-role-key}
|
||||
AUTH_JWT_SECRET=${secret:jwt}
|
||||
LOGFLARE_API_KEY=${secret:logflare}
|
||||
LOGFLARE_URL=http://supabase-analytics:4000
|
||||
NEXT_PUBLIC_ENABLE_LOGS=true
|
||||
NEXT_ANALYTICS_BACKEND_PROVIDER=postgres
|
||||
@@ -0,0 +1,258 @@
|
||||
# Written by the mesh (modules/supabase).
|
||||
#
|
||||
# Every sink hands logflare its API key in the x-api-key header, never as an api_key query parameter:
|
||||
# logflare prints a failed request's whole URL, query string included, in its error report, so a
|
||||
# key in the URL is a key in its log (novox/hq issue 268). supabase-analytics refuses to start
|
||||
# while this file still puts the key in a URL.
|
||||
api:
|
||||
enabled: true
|
||||
address: 0.0.0.0:9001
|
||||
|
||||
sources:
|
||||
docker_host:
|
||||
type: docker_logs
|
||||
include_containers:
|
||||
- supabase-kong
|
||||
- supabase-auth
|
||||
- supabase-rest
|
||||
- realtime-dev.supabase-realtime
|
||||
- supabase-storage
|
||||
- supabase-edge-functions
|
||||
- supabase-db
|
||||
|
||||
transforms:
|
||||
project_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- docker_host
|
||||
source: |-
|
||||
.project = "default"
|
||||
.event_message = del(.message)
|
||||
.appname = del(.container_name)
|
||||
del(.container_created_at)
|
||||
del(.container_id)
|
||||
del(.source_type)
|
||||
del(.stream)
|
||||
del(.label)
|
||||
del(.image)
|
||||
del(.host)
|
||||
del(.stream)
|
||||
router:
|
||||
type: route
|
||||
inputs:
|
||||
- project_logs
|
||||
route:
|
||||
kong: '.appname == "supabase-kong"'
|
||||
auth: '.appname == "supabase-auth"'
|
||||
rest: '.appname == "supabase-rest"'
|
||||
realtime: '.appname == "realtime-dev.supabase-realtime"'
|
||||
storage: '.appname == "supabase-storage"'
|
||||
functions: '.appname == "supabase-edge-functions"'
|
||||
db: '.appname == "supabase-db"'
|
||||
# Ignores non nginx errors since they are related with kong booting up
|
||||
kong_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.kong
|
||||
source: |-
|
||||
req, err = parse_nginx_log(.event_message, "combined")
|
||||
if err == null {
|
||||
.timestamp = req.timestamp
|
||||
.metadata.request.headers.referer = req.referer
|
||||
.metadata.request.headers.user_agent = req.agent
|
||||
.metadata.request.headers.cf_connecting_ip = req.client
|
||||
.metadata.request.method = req.method
|
||||
.metadata.request.path = req.path
|
||||
.metadata.request.protocol = req.protocol
|
||||
.metadata.response.status_code = req.status
|
||||
}
|
||||
if err != null {
|
||||
abort
|
||||
}
|
||||
# Ignores non nginx errors since they are related with kong booting up
|
||||
kong_err:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.kong
|
||||
source: |-
|
||||
.metadata.request.method = "GET"
|
||||
.metadata.response.status_code = 200
|
||||
parsed, err = parse_nginx_log(.event_message, "error")
|
||||
if err == null {
|
||||
.timestamp = parsed.timestamp
|
||||
.severity = parsed.severity
|
||||
.metadata.request.host = parsed.host
|
||||
.metadata.request.headers.cf_connecting_ip = parsed.client
|
||||
url, err = split(parsed.request, " ")
|
||||
if err == null {
|
||||
.metadata.request.method = url[0]
|
||||
.metadata.request.path = url[1]
|
||||
.metadata.request.protocol = url[2]
|
||||
}
|
||||
}
|
||||
if err != null {
|
||||
abort
|
||||
}
|
||||
# Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency.
|
||||
auth_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.auth
|
||||
source: |-
|
||||
parsed, err = parse_json(.event_message)
|
||||
if err == null {
|
||||
.metadata.timestamp = parsed.time
|
||||
.metadata = merge!(.metadata, parsed)
|
||||
}
|
||||
# PostgREST logs are structured so we separate timestamp from message using regex
|
||||
rest_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.rest
|
||||
source: |-
|
||||
parsed, err = parse_regex(.event_message, r'^(?P<time>.*): (?P<msg>.*)$')
|
||||
if err == null {
|
||||
.event_message = parsed.msg
|
||||
.timestamp = to_timestamp!(parsed.time)
|
||||
.metadata.host = .project
|
||||
}
|
||||
# Realtime logs are structured so we parse the severity level using regex (ignore time because it has no date)
|
||||
realtime_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.realtime
|
||||
source: |-
|
||||
.metadata.project = del(.project)
|
||||
.metadata.external_id = .metadata.project
|
||||
parsed, err = parse_regex(.event_message, r'^(?P<time>\d+:\d+:\d+\.\d+) \[(?P<level>\w+)\] (?P<msg>.*)$')
|
||||
if err == null {
|
||||
.event_message = parsed.msg
|
||||
.metadata.level = parsed.level
|
||||
}
|
||||
# Storage logs may contain json objects so we parse them for completeness
|
||||
storage_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.storage
|
||||
source: |-
|
||||
.metadata.project = del(.project)
|
||||
.metadata.tenantId = .metadata.project
|
||||
parsed, err = parse_json(.event_message)
|
||||
if err == null {
|
||||
.event_message = parsed.msg
|
||||
.metadata.level = parsed.level
|
||||
.metadata.timestamp = parsed.time
|
||||
.metadata.context[0].host = parsed.hostname
|
||||
.metadata.context[0].pid = parsed.pid
|
||||
}
|
||||
# Postgres logs some messages to stderr which we map to warning severity level
|
||||
db_logs:
|
||||
type: remap
|
||||
inputs:
|
||||
- router.db
|
||||
source: |-
|
||||
.metadata.host = "db-default"
|
||||
.metadata.parsed.timestamp = .timestamp
|
||||
|
||||
parsed, err = parse_regex(.event_message, r'.*(?P<level>INFO|NOTICE|WARNING|ERROR|LOG|FATAL|PANIC?):.*', numeric_groups: true)
|
||||
|
||||
if err != null || parsed == null {
|
||||
.metadata.parsed.error_severity = "info"
|
||||
}
|
||||
if parsed != null {
|
||||
.metadata.parsed.error_severity = parsed.level
|
||||
}
|
||||
if .metadata.parsed.error_severity == "info" {
|
||||
.metadata.parsed.error_severity = "log"
|
||||
}
|
||||
.metadata.parsed.error_severity = upcase!(.metadata.parsed.error_severity)
|
||||
|
||||
sinks:
|
||||
logflare_auth:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- auth_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 10
|
||||
headers:
|
||||
x-api-key: '${secret:logflare}'
|
||||
uri: 'http://supabase-analytics:4000/api/logs?source_name=gotrue.logs.prod'
|
||||
logflare_realtime:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- realtime_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 10
|
||||
headers:
|
||||
x-api-key: '${secret:logflare}'
|
||||
uri: 'http://supabase-analytics:4000/api/logs?source_name=realtime.logs.prod'
|
||||
logflare_rest:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- rest_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 10
|
||||
headers:
|
||||
x-api-key: '${secret:logflare}'
|
||||
uri: 'http://supabase-analytics:4000/api/logs?source_name=postgREST.logs.prod'
|
||||
logflare_db:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- db_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 10
|
||||
headers:
|
||||
x-api-key: '${secret:logflare}'
|
||||
# We must route the sink through kong because ingesting logs before logflare is fully initialised will
|
||||
# lead to broken queries from studio. This works by the assumption that containers are started in the
|
||||
# following order: vector > db > logflare > kong
|
||||
uri: 'http://supabase-kong:8000/analytics/v1/api/logs?source_name=postgres.logs'
|
||||
logflare_functions:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- router.functions
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 10
|
||||
headers:
|
||||
x-api-key: '${secret:logflare}'
|
||||
uri: 'http://supabase-analytics:4000/api/logs?source_name=deno-relay-logs'
|
||||
logflare_storage:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- storage_logs
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 10
|
||||
headers:
|
||||
x-api-key: '${secret:logflare}'
|
||||
uri: 'http://supabase-analytics:4000/api/logs?source_name=storage.logs.prod.2'
|
||||
logflare_kong:
|
||||
type: 'http'
|
||||
inputs:
|
||||
- kong_logs
|
||||
- kong_err
|
||||
encoding:
|
||||
codec: 'json'
|
||||
method: 'post'
|
||||
request:
|
||||
retry_max_duration_secs: 10
|
||||
headers:
|
||||
x-api-key: '${secret:logflare}'
|
||||
uri: 'http://supabase-analytics:4000/api/logs?source_name=cloudflare.logs.prod'
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user