diff options
64 files changed, 1602 insertions, 183 deletions
diff --git a/apps/dreki/include/dreki_otel.hrl b/apps/dreki/include/dreki_otel.hrl new file mode 100644 index 0000000..e784874 --- /dev/null +++ b/apps/dreki/include/dreki_otel.hrl @@ -0,0 +1,11 @@ +-include_lib("opentelemetry_api/include/opentelemetry.hrl"). +-include_lib("opentelemetry_api/include/otel_tracer.hrl"). + +-define(FUN_NAME, binary:list_to_bin([ + atom_to_binary(?MODULE), <<":">>, + atom_to_binary(?FUNCTION_NAME), <<"/">>, + integer_to_binary(?FUNCTION_ARITY) + ]) +). + +-define(FUN_NAME(D), binary:list_to_bin([?FUN_NAME, atom_to_binary(D)])). diff --git a/apps/dreki/include/dreki_plum.hrl b/apps/dreki/include/dreki_plum.hrl index bea65aa..aed2e05 100644 --- a/apps/dreki/include/dreki_plum.hrl +++ b/apps/dreki/include/dreki_plum.hrl @@ -5,14 +5,13 @@ %% dreki_store tabs -define(PLUM_DB_STORE_TASKS_TAB, dreki_tasks). - %% indices tabs -define(PLUM_DB_IDX_ROLE_TAB, 'dreki_idx:roles'). -define(PLUM_DB_IDX_TAGS_TAB, 'dreki_idx:tags'). -define(PLUM_DB_PREFIXES, [ - {?PLUM_DB_REGIONS_TAB, ram_disk}, - {?PLUM_DB_NODES_TAB, ram_disk}, + {?PLUM_DB_REGIONS_TAB, disk}, + {?PLUM_DB_NODES_TAB, disk}, {?PLUM_DB_PATHS_TAB, ram_disk}, {?PLUM_DB_STORES_TAB, ram_disk}, @@ -23,4 +22,3 @@ {?PLUM_DB_IDX_ROLE_TAB, disk}, {?PLUM_DB_IDX_TAGS_TAB, disk} ]). - diff --git a/apps/dreki/src/dreki.app.src b/apps/dreki/src/dreki.app.src index 78009bf..2e152a7 100644 --- a/apps/dreki/src/dreki.app.src +++ b/apps/dreki/src/dreki.app.src @@ -8,9 +8,15 @@ stdlib, mnesia, logger_colorful, + ipee, opentelemetry, opentelemetry_api, opentelemetry_exporter, + opentelemetry_logger_metadata, + telemetry, + opentelemetry_telemetry, + prometheus, + genlib, uuid, mnesia_rocksdb, plum_db, diff --git a/apps/dreki/src/dreki_app.erl b/apps/dreki/src/dreki_app.erl index a1259a7..0442b4c 100644 --- a/apps/dreki/src/dreki_app.erl +++ b/apps/dreki/src/dreki_app.erl @@ -31,6 +31,8 @@ before_start(Type, Args) -> logger:set_application_level(dreki_web, debug), logger:set_application_level(partisan, info), logger:set_application_level(plum_db, info), + logger:set_handler_config(default, level, debug), + opentelemetry_logger_metadata:setup(), ?LOG_NOTICE(#{message => "Dreki starting...."}), application:stop(partisan), ok = dreki_config:init(Args), @@ -48,6 +50,7 @@ after_start() -> ok = setup_event_manager(), ok = dreki_store:start(), ?LOG_NOTICE(#{message => "Dreki Ready"}), + ok = dreki_config:set([dreki, status], ready), dreki_event_manager:notify(dreki_ready), ok. @@ -70,12 +73,11 @@ setup_event_manager() -> Mod = partisan_peer_service:manager(), Mod:on_up('_', fun(Node) -> - dreki_event_manager:notify({peer_up, Node}) + dreki_event_manager:notify({partisan_peer_service, peer_up, Node}) end), Mod:on_down('_', fun(Node) -> - dreki_event_manager:notify({peer_down, Node}) + dreki_event_manager:notify({partisan_peer_service, peer_down, Node}) end), ok. - diff --git a/apps/dreki/src/dreki_config.erl b/apps/dreki/src/dreki_config.erl index 69df2d6..457f559 100644 --- a/apps/dreki/src/dreki_config.erl +++ b/apps/dreki/src/dreki_config.erl @@ -2,8 +2,14 @@ -include_lib("kernel/include/logger.hrl"). -include_lib("partisan/include/partisan.hrl"). -include("dreki_plum.hrl"). +-include("dreki_otel.hrl"). + +-compile({no_auto_import,[get/0]}). + -export([init/1]). +-export([get/1]). +-export([set/2]). -define(CONFIG, [ %% All stolen from bondy as partisan isn't that well documented, eh. @@ -44,15 +50,29 @@ -define(PT, dreki_config_cache). init(_Args) -> - persistent_term:put(?PT, application:get_all_env(dreki)), + persistent_term:put(?PT, [{dreki, application:get_all_env(dreki)}, + {dreki_web, application:get_all_env(dreki_web) + }]), ok = set_app_configs(?CONFIG), - ?LOG_INFO(#{message => "Configured Dreki and dependencies"}), + ?LOG_NOTICE(#{message => "Configured Dreki and dependencies"}), ok = partisan_config:init(), ok. +get() -> + persistent_term:get(?PT). + +get(Key) -> + ?with_span(?FUN_NAME, #{}, fun (_) -> key_value:get(Key, get()) end). + +set(Key, Value) -> + ?with_span(?FUN_NAME, #{}, + fun (_) -> + persistent_term:put(?PT, key_value:put(Key, Value, get())), + ok + end). + set_app_configs(Configs) -> lists:foreach(fun ({App, Params}) -> [application:set_env(App, Key, Value) || {Key, Value} <- Params] end, Configs), ok. - diff --git a/apps/dreki/src/dreki_freebsd_schemas.erl b/apps/dreki/src/dreki_freebsd_schemas.erl new file mode 100644 index 0000000..de97ded --- /dev/null +++ b/apps/dreki/src/dreki_freebsd_schemas.erl @@ -0,0 +1,59 @@ +-module(dreki_freebsd_schemas). + +-export([schema/1]). + +schema(<<"dreki.v1.freebsd.jails.list">>) -> + #{ + <<"@schema">> => "dreki.v1.freebsd.jails.list", + version => 'draft-06', + title => <<"List running jails ID/Names">>, + properties => #{} + }; +schema(<<"dreki.v1.freebsd.jails.get">>) -> + #{ + <<"@schema">> => "dreki.v1.freebsd.jails.get", + version => 'draft-06', + title => <<"Get jail information">>, + properties => #{ + jail_id => #{type => integer} + }, + required => [jail_id] + }; +schema(<<"dreki.v1.freebsd.jails.start">>) -> + #{ + <<"@schema">> => "dreki.v1.freebsd.jails.start", + version => 'draft-06', + title => <<"Start a jail">>, + properties => #{ + name => #{type => string}, + path => #{type => string}, + hostname => #{type => string}, + params => #{type => object} + }, + required => [name, path, hostname, params] + }; +schema(<<"dreki.v1.freebsd.jails.exec">>) -> + #{ + <<"@schema">> => "dreki.v1.freebsd.jails.exec", + version => 'draft-06', + title => <<"Execute a command in a jail">>, + properties => #{ + jail_id => #{type => string}, + command => #{type => string}, + args => #{type => array}, + env => #{type => object} + }, + required => [id, command, args, env] + }; +schema(<<"dreki.v1.exec">>) -> + #{ + <<"@schema">> => "dreki.v1.exec", + version => 'draft-06', + title => <<"Execute a command">>, + properties => #{ + command => #{type => string}, + args => #{type => array}, + env => #{type => object} + }, + required => [command, args, env] + }. diff --git a/apps/dreki/src/dreki_plum.erl b/apps/dreki/src/dreki_plum.erl index 9c03e46..dccdad7 100644 --- a/apps/dreki/src/dreki_plum.erl +++ b/apps/dreki/src/dreki_plum.erl @@ -8,7 +8,6 @@ before_start() -> %% We temporarily disable plum_db's AAE to avoid rebuilding hashtrees %% until we are ready to do it ok = suspend_aae(), - logger:debug("-- DISABLED AAE ! --"), _ = application:ensure_all_started(plum_db, permanent), ok. @@ -27,9 +26,7 @@ suspend_aae() -> true -> ok = application:set_env(plum_db, priv_aae_enabled, true), ok = application:set_env(plum_db, aae_enabled, false), - ?LOG_NOTICE(#{ - description => "Temporarily disabled active anti-entropy (AAE) during initialisation" - }), + ?LOG_NOTICE(#{message => "Temporarily disabled plum_db aae during initialisation"}), ok; false -> ok @@ -40,9 +37,7 @@ restore_aae() -> true -> %% plum_db should have started so we call plum_db_config ok = plum_db_config:set(aae_enabled, true), - ?LOG_NOTICE(#{ - description => "Active anti-entropy (AAE) re-enabled" - }), + ?LOG_NOTICE(#{message => "plum_db aae re-enabled"}), ok; false -> ok @@ -51,11 +46,10 @@ restore_aae() -> maybe_wait_for_plum_db_partitions() -> case wait_for_partitions() of true -> - %% We block until all partitions are initialised - ?LOG_NOTICE(#{ - description => "Application master is waiting for plum_db partitions to be initialised" - }), - plum_db_startup_coordinator:wait_for_partitions(); + ?LOG_NOTICE(#{domain => [dreki_plum], message => "Waiting for plum_db partitions to be initialised"}), + ok = plum_db_startup_coordinator:wait_for_partitions(), + ?LOG_NOTICE(#{domain => [dreki_plum], message => "plum_db partitions initialised"}), + ok; false -> ok end. @@ -64,10 +58,10 @@ maybe_wait_for_plum_db_hashtrees() -> case wait_for_hashtrees() of true -> %% We block until all hashtrees are built - ?LOG_NOTICE(#{ - description => "Application master is waiting for plum_db hashtrees to be built" - }), - plum_db_startup_coordinator:wait_for_hashtrees(); + ?LOG_NOTICE(#{domain => [dreki_plum], message => "Waiting for plum_db hashtrees to be built"}), + ok = plum_db_startup_coordinator:wait_for_hashtrees(), + ?LOG_NOTICE(#{domain => [dreki_plum], message => "plum_db hashtrees built"}), + ok; false -> ok end, @@ -88,9 +82,7 @@ maybe_wait_for_aae_exchange() -> %% We have not yet joined a cluster, so we finish ok; Peers -> - ?LOG_NOTICE(#{ - description => "Application master is waiting for plum_db AAE to perform exchange" - }), + ?LOG_NOTICE(#{domain => [plum_db], message => "Waiting for plum_db AAE to perform exchange"}), %% We are in a cluster, we randomnly pick a peer and %% perform an AAE exchange [Peer|_] = lists_utils:shuffle(Peers), diff --git a/apps/dreki/src/dreki_sup.erl b/apps/dreki/src/dreki_sup.erl index c1aa636..60b33e5 100644 --- a/apps/dreki/src/dreki_sup.erl +++ b/apps/dreki/src/dreki_sup.erl @@ -16,20 +16,12 @@ start_link() -> supervisor:start_link({local, ?SERVER}, ?MODULE, []). -%% sup_flags() = #{strategy => strategy(), % optional -%% intensity => non_neg_integer(), % optional -%% period => pos_integer()} % optional -%% child_spec() = #{id => child_id(), % mandatory -%% start => mfargs(), % mandatory -%% restart => restart(), % optional -%% shutdown => shutdown(), % optional -%% type => worker(), % optional -%% modules => modules()} % optional init([]) -> SupFlags = #{strategy => one_for_all, intensity => 10, period => 5}, ChildSpecs = [ + #{id => drekid, start => {drekid, start_link, [[]]}}, #{id => dreki_event_manager, start => {dreki_event_manager, start_link, [[]]}}, #{id => dreki_world_server, start => {dreki_world_server, start_link, [[]]}}, #{id => dreki_node_server, start => {dreki_node_server, start_link, [[]]}} diff --git a/apps/dreki/src/drekid.erl b/apps/dreki/src/drekid.erl new file mode 100644 index 0000000..73beffd --- /dev/null +++ b/apps/dreki/src/drekid.erl @@ -0,0 +1,95 @@ +-module(drekid). +-behaviour(partisan_gen_fsm). +-include_lib("kernel/include/logger.hrl"). +-include_lib("dreki_otel.hrl"). +-compile({no_auto_import, [get/0]}). +-export([get/0]). +-export([request/1, request/2, request/3]). +-export([start_link/1]). +-export([init/1, waiting/2, handle_info/3, handle_event/3, handle_sync_event/4]). +-define(DREKID_PT, drekid). +-record(drekid, {node, pid}). + +-spec get() -> {ok, pid()} | {error, drekid_unavailable}. +get() -> + persistent_term:get(drekid, {error, drekid_unavailable}). + +request(Fun) -> + request(Fun, []). + +request(Fun, Args) -> + request(Fun, Args, 5000). + +request(Fun, Args, Timeout) when is_binary(Fun) -> + ?with_span(?FUN_NAME, fun(_SpanCtx) -> + send_request(Fun, Args, Timeout) + end). + +send_request(Fun, Args, Timeout) -> + ?set_attributes(#{function => Fun, args => Args, timeout => Timeout}), + case get() of + {ok, Pid} -> + Ref = make_ref(), + logger:info("drekid_request: ~p ~p to ~p", [Fun, Args, Pid]), + Pid ! {drekid_request_v1, self(), Ref, Fun, Args, Timeout}, + ?with_span(?FUN_NAME(await_request), fun(_) -> + await_request(Ref, Timeout) + end); + Error -> + Error + end. + +await_request(Ref, Timeout) -> + receive + {drekid_response_v1, Ref, Result} -> + case Result of + {ok, Data = {_, _}} -> + Data; + {ok, Data} -> + {ok, Data}; + {error, Error} -> + {error, {drekid_error, Error}} + end + after Timeout -> + {error, {drekid_error, timeout}} + end. + +start_link(Args) -> + partisan_gen_fsm:start_link({local, ?MODULE}, ?MODULE, Args, []). + +%% + +init(_Args) -> + ?LOG_DEBUG("drekid initialized"), + {ok, waiting, #drekid{}, 0}. + +waiting(timeout, Data) -> + persistent_term:put(drekid, {error, drekid_unavailable}), + {next_state, waiting, Data}; +waiting({drekid, {connect, Node, Pid}}, Data) -> + Data0 = Data#drekid{node = Node, pid = Pid}, + monitor_node(Node, true), + Pid ! {drekid, hello, node(), self()}, + ?LOG_NOTICE("drekid ready, node: ~p ~p", [Node, Pid]), + persistent_term:put(drekid, {ok, Pid}), + {next_state, ready, Data}. + +down(timeout, {Reason, State, Data}) -> + ?LOG_ERROR(#{code => drekid_down, reason => Reason, state => State}), + {next_state, waiting, Data#drekid{node = undefined, pid = undefined}}. + +handle_info({drekid, Msg}, waiting, Data) -> + waiting({drekid, Msg}, Data); +handle_info({nodedown, Node}, State, Data = #drekid{node = Node}) -> + {next_state, down, {nodedown, State, Data}, 0}; +handle_info(Info, State, Data) -> + ?LOG_ERROR(#{code => unhandled_info, state => State, info => Info}), + {stop, badinfo, Data}. + +handle_event(Event, State, Data) -> + ?LOG_ERROR(#{code => unhandled_event, state => State, event => Event}), + {stop, badevent, Data}. + +handle_sync_event(Event, From, State, Data) -> + ?LOG_ERROR(#{code => unhandled_sync_event, state => State, event => Event}), + {stop, badevent, badevent, Data}. diff --git a/apps/dreki/src/drekid_tasks_store.erl b/apps/dreki/src/drekid_tasks_store.erl new file mode 100644 index 0000000..e16182a --- /dev/null +++ b/apps/dreki/src/drekid_tasks_store.erl @@ -0,0 +1,71 @@ +-module(drekid_tasks_store). +-behaviour(dreki_store_backend). + +-export([install/0]). +-export([start/0, start/5, checkout/1, checkin/1, stop/0, stop/1]). +-export([valid_store/5]). +-export([list/1, count/1, exists/2, get/2, create/2, update/2, delete/2]). + +tasks() -> + Tasks = #{<<"dreki.v1.exec">> => #{id => <<"dreki.v1.exec">>, module => drekid_task_handler, params => #{}}}, + FreeBSD = [<<"dreki.v1.freebsd.jails.list">>, + <<"dreki.v1.freebsd.jails.get">>, + <<"dreki.v1.freebsd.jails.start">>, + <<"dreki.v1.freebsd.jails.exec">> + ], + Tasks0 = lists:foldr(fun (Id, Acc) -> + Task = #{id => Id, module => drekid_task_handler, params => dreki_freebsd_schemas:schema(Id)}, + maps:put(Id, Task, Acc) + end, Tasks, FreeBSD), + Tasks0. + +urn() -> + Urn = dreki_node:urn(), + <<Urn/binary, "::tasks:drekid">>. + +install() -> + dreki_store:create_store(urn(), ?MODULE, #{}, #{}). + +valid_store(<<"tasks">>, _Loc, _Name, _NSMod, _Args) -> + ok; +valid_store(_Ns, _Loc, _Name, _NSMod, _Args) -> + error. + +start() -> + ok. + +start(_Ns, _NsMod, _Loc, _XUrn, Args) -> + {ok, Args}. + +checkout(Args) -> + {ok, Args}. + +checkin(Args) -> + ok. + +stop() -> + ok. + +stop(Args) -> + ok. + +list(_) -> + {ok, [V || {K, V} <- maps:to_list(tasks())]}. + +get(_, Id) -> + maps:get(Id, tasks(), not_found). + +count(_) -> + {ok, maps:size(tasks())}. + +exists(_, Id) -> + {ok, maps:is_key(Id, tasks())}. + +create(_, _) -> + {error, not_supported}. + +update(_, _) -> + {error, not_supported}. + +delete(_, _) -> + {error, not_supported}. diff --git a/apps/dreki/src/funs/dreki_fun_exec.erl b/apps/dreki/src/funs/dreki_fun_exec.erl new file mode 100644 index 0000000..d704e80 --- /dev/null +++ b/apps/dreki/src/funs/dreki_fun_exec.erl @@ -0,0 +1,28 @@ +-module(dreki_fun_exec). +-behaviour(dreki_funs). +-export([schemas/0]). + +schemas() -> + #{<<"exec">> => #{default_version => <<"1.0">>, <<"1.0">> => schemas('1.0')}}. + +schemas('1.0') -> + #{ + version => 'draft-06', + title => <<"Execute command">>, + type => object, + properties => #{ + <<"id">> => #{type => string, + <<"dreki:form">> => #{default => generate_id}}, + <<"name">> => #{type => string}, + <<"description">> => #{type => string, + <<"dreki:form">> => #{ + input => textarea, + textarea_mode => markdown + }}, + <<"command">> => #{type => string, <<"dreki:form">> => #{ + input_style => monospace, + placeholder => <<"/bin/true">> + }} + }, + required => [id, name, command] + }. diff --git a/apps/dreki/src/funs/dreki_funs.erl b/apps/dreki/src/funs/dreki_funs.erl new file mode 100644 index 0000000..1d15f21 --- /dev/null +++ b/apps/dreki/src/funs/dreki_funs.erl @@ -0,0 +1,31 @@ +-module(dreki_funs). +-include("dreki.hrl"). + +-behaviour(dreki_store_namespace). +-export([start/0, valid_store/4, format_item/1, schemas/0]). + +-callback(schemas() -> #{}). + +start() -> + ok. + +valid_store(_Namespace, _Location, _Name, _BackendMod) -> + ok. + +format_item(Item) -> + ok. + +handlers() -> + [dreki_fun_exec]. + +-record(?MODULE, { + id, + version, + name, + handler, + content + }). + +schemas() -> + Schemas = lists:foldr(fun (Handler, Acc) -> maps:merge(Acc, Handler:schemas()) end, #{}, handlers()), + maps:put(default, <<"exec:1.0">>, Schemas). diff --git a/apps/dreki/src/dreki_node.erl b/apps/dreki/src/node/dreki_node.erl index 87dbc73..5ed7fac 100644 --- a/apps/dreki/src/dreki_node.erl +++ b/apps/dreki/src/node/dreki_node.erl @@ -1,10 +1,11 @@ -module(dreki_node). -include("dreki.hrl"). --include_lib("opentelemetry_api/include/otel_tracer.hrl"). +-include_lib("kernel/include/logger.hrl"). +-include("dreki_otel.hrl"). -behaviour(partisan_gen_fsm). -compile({no_auto_import,[get/0]}). -export([get/0, urn/0, stores/0]). --export([rpc/4, rpc/5]). +-export([rpc/4, rpc/5, remote_rpc/7]). -export([uri/0]). % deprecated -export([parents/0, parents/1, parent/0, parent/1]). -export([neighbours/0, neighbours/1]). @@ -18,17 +19,64 @@ rpc(Path, Mod, Fun, Args) -> -spec rpc(dreki_urn(), module(), function(), Args :: [], #{timeout => non_neg_integer()}) -> {ok, any()} | {error, rpc_error()}. rpc(Path, Mod, Fun, Args, #{timeout := Timeout}) -> - ?with_span(<<"dreki_node:rpc">>, #{}, fun(ChildSpanCtx) -> + Myself = self(), + ?with_span(?FUN_NAME, fun(SpanCtx) -> + ?set_attributes(#{mod => Mod, fn => Fun, remote_node_path => Path}), + ?LOG_INFO("Ctx ~p ~p ~p ~p", [?FUN_NAME, otel_ctx:get_current(), otel_tracer:current_span_ctx(), SpanCtx]), case dreki_world_dns:node_param(dreki_world:path_to_domain(Path), node_name) of - {ok, NodeName} -> - case partisan_rpc_backend:call(NodeName, Mod, Fun, Args, Timeout) of - {badrpc, Error} -> {error, {rpc_error, Path, Error}}; - Result -> Result - end; - Error -> Error - end + {ok, NodeName} -> + ?set_attribute(remote_node, NodeName), + Ctx = otel_ctx:get_current(), + ChildSpanCtx = ?start_span(?FUN_NAME(remote), #{}), + TraceId = otel_span:hex_trace_id(SpanCtx), + SpanId = otel_span:hex_span_id(SpanCtx), + case partisan_rpc_backend:call(NodeName, ?MODULE, remote_rpc, [Myself, Mod, Fun, Args, Timeout, TraceId, SpanId], Timeout) of + {badrpc, RpcError} -> + ?set_status(error, <<"RPC_ERROR">>), + {error, {rpc, RpcError}}; + Error = {error, _} -> + ?set_status(error, <<"RESULT">>), + Error; + Result -> + ?set_status(ok), + Result + end; + Error -> + ?set_status(error, <<"NODE_NOT_FOUND">>), + Error + end end). +remote_rpc(Pid, Mod, Fun, Args, Timeout, TraceId, SpanId) -> + ParentCtx = otel_tracer:from_remote_span(TraceId, SpanId, 1), + otel_tracer:with_span(ParentCtx, opentelemetry:get_tracer(), ?FUN_NAME, #{}, + fun (Ctx) -> remote_rpc(Pid, Mod, Fun, Args, Timeout, Ctx) end). + +remote_rpc(Pid, Mod, Fun, Args, Timeout, Ctx) -> + ?set_attributes(#{remote_node => node(), module => Mod, function => Fun}), + try apply(Mod, Fun, Args) of + {badrpc, RpcError} -> + ?set_status(error, <<"RPC_ERROR">>), + {error, {rpc, RpcError}}; + Error = {error, _} -> + ?set_status(error, <<"RESULT">>), + Error; + Result -> + Result + catch + throw:Term:Stacktrace -> + ?set_status(error, <<"THROW">>), + {error, {throw, Term, Stacktrace}}; + exit:Reason:Stacktrace -> + ?set_status(error, <<"EXIT">>), + {error, {exit, Reason, Stacktrace}}; + error:Reason:Stacktrace -> + ?set_status(error, <<"RUNTIME">>), + {error, {runtime_error, Reason, Stacktrace}} + after + ?end_span() + end. + get() -> {ok, Node} = dreki_world:get_node(dreki_world:node()), Node. @@ -100,4 +148,3 @@ ensure_local_node() -> create_local_node() -> dreki_world:create_node(uri(), #{}). - diff --git a/apps/dreki/src/dreki_node_server.erl b/apps/dreki/src/node/dreki_node_server.erl index c8bca51..c8bca51 100644 --- a/apps/dreki/src/dreki_node_server.erl +++ b/apps/dreki/src/node/dreki_node_server.erl diff --git a/apps/dreki/src/node/rebar.conf b/apps/dreki/src/node/rebar.conf new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/apps/dreki/src/node/rebar.conf diff --git a/apps/dreki/src/requests/dreki_requests.erl b/apps/dreki/src/requests/dreki_requests.erl new file mode 100644 index 0000000..d970dbd --- /dev/null +++ b/apps/dreki/src/requests/dreki_requests.erl @@ -0,0 +1,97 @@ +-module(dreki_requests). +-behaviour(dreki_store_namespace). +-export([start/0, version/0, valid_store/4, format_item/1, actions/0, actions/1, schemas/0, new/0, as_tuple/1, as_map/1]). +-export([record_name/0, record_attributes/0]). +-export([after_create/1]). +-export([new/1, create/2]). +-export([install/0]). + +-record(?MODULE, { + id, + version, + schema, + task_urn, + identity_urn, + node, + params + }). + +create(#{'@ns' := <<"tasks">>, '@location' := Loc, '@id' := TaskUrn}, Params0) -> + LocalStore = <<Loc/binary, "::requests:local">>, + Params = Params0#{task_urn => TaskUrn}, + dreki_store:create(LocalStore, Params). + +install() -> + NodeUrn = dreki_node:urn(), + Urn = <<NodeUrn/binary, "::requests:local">>, + dreki_store:create_store(Urn, dreki_mnesia_store, #{}, #{}). + +record_name() -> + ?MODULE. + +record_attributes() -> + record_info(fields, ?MODULE). + +version() -> + 1. + +start() -> + ok. + +valid_store(_Ns, _Loc, _Name, _BackendMod) -> + ok. + +format_item(Item) -> + ok. + +actions() -> + []. + +actions(_) -> + []. + +schemas() -> + #{default => <<"request">>, + <<"request">> => #{ + default_version => <<"1.0">>, + <<"1.0">> => schemas(<<"request">>, <<"1.0">>) + } + }. + +schemas(<<"request">>, <<"1.0">>) -> + #{ + version => 'draft-06', + title => <<"Run Request">>, + type => object, + properties => #{ + id => #{type => string, <<"dreki:form">> => #{readonly => true}}, + node => #{type => string}, + params => #{type => object} + }, + required => [node, params] + }. + +new() -> + schemas(<<"request">>, <<"1.0">>). + +new(#{'@ns' := <<"tasks">>, '@id' := TaskUrn, id := TaskId, module := Module, params := TaskParams}) -> + Schema0 = schemas(<<"request">>, <<"1.0">>), + Schema = key_value:set([properties, params], TaskParams, Schema0), + {ok, Schema}. + +as_tuple(#{id := Id, task_urn := TaskUrn, node := Node, params := Params}) -> + #?MODULE{id = Id, version = undefined, schema = undefined, task_urn = TaskUrn, + node = Node, params = Params}. + +as_map(R = #?MODULE{}) -> + #{ + id => R#?MODULE.id, + version => R#?MODULE.version, + schema => R#?MODULE.schema, + task_urn => R#?MODULE.task_urn, + node => R#?MODULE.node, + params => R#?MODULE.params + }. + +after_create(Request) -> + ok. diff --git a/apps/dreki/src/runs/dreki_runs.erl b/apps/dreki/src/runs/dreki_runs.erl new file mode 100644 index 0000000..9509fa6 --- /dev/null +++ b/apps/dreki/src/runs/dreki_runs.erl @@ -0,0 +1 @@ +-module(dreki_runs). diff --git a/apps/dreki/src/storages/dreki_storages.erl b/apps/dreki/src/storages/dreki_storages.erl new file mode 100644 index 0000000..3a87347 --- /dev/null +++ b/apps/dreki/src/storages/dreki_storages.erl @@ -0,0 +1,98 @@ +-module(dreki_storages). +-behaviour(dreki_store_namespace). +-export([start/0, version/0, valid_store/4, format_item/1, actions/0, actions/1, schemas/0, as_tuple/1, as_map/1]). +-export([record_name/0, record_attributes/0]). +-export([after_create/1]). +-export([install_local_store/0, create_local_storage/4]). + +-record(?MODULE, { + id, + version, + schema, + name, + type, + handler, + params, + tags = [] + }). + +install_local_store() -> + NodeUrn = dreki_node:urn(), + Urn = <<NodeUrn/binary, "::storages:local">>, + dreki_store:create_store(Urn, dreki_mnesia_store, #{}, #{}). + +create_local_storage(Type, Handler, Params, Tags) -> + NodeUrn = dreki_node:urn(), + LocalStoreUrn = <<NodeUrn/binary, "::storages:local">>, + Storage = #{ + type => Type, + handler => Handler, + params => Params, + tags => Tags + }, + dreki_store:create(LocalStoreUrn, Storage). + +record_name() -> ?MODULE. +record_attributes() -> record_info(fields, ?MODULE). +version() -> 1. + +start() -> + ok. + +valid_store(_Ns, _Loc, _Name, _BackendMod) -> + ok. + +format_item(Item) -> + ok. + +actions() -> + []. + +actions(_) -> + []. + +schemas() -> + #{default => <<"storage">>, + <<"storage">> => #{ + default_version => <<"1.0">>, + <<"1.0">> => schemas(<<"storage">>, <<"1.0">>) + } + }. + +schemas(<<"storage">>, <<"1.0">>) -> + #{ + version => 'draft-06', + title => <<"Storage">>, + type => object, + required => [id, type, handler, params], + properties => #{ + id => #{type => string, <<"dreki:form">> => #{}}, + name => #{type => string}, + type => #{type => string, enum => [<<"fs">>, <<"os">>, <<"bs">>]}, + handler => #{type => string, enum => [<<"zfs">>, <<"fs">>]}, + params => #{type => object}, + tags => #{type => array, items => #{type => string}} + } + }. + +as_tuple(Map = #{id := Id, type := Type, handler := Handler, params := Params, tags := Tags}) -> + #?MODULE{id = Id, + name = maps:get(name, Map, undefined), + type = Type, + handler = Handler, + params = Params, + tags = maps:get(tags, Map, []) + }. + +as_map(R = #?MODULE{}) -> + #{ + id => R#?MODULE.id, + name => R#?MODULE.name, + type => R#?MODULE.type, + handler => R#?MODULE.handler, + params => R#?MODULE.params, + tags => R#?MODULE.tags + }. + +after_create(_Storage) -> + ok. diff --git a/apps/dreki/src/storages/dreki_storages_zfs.erl b/apps/dreki/src/storages/dreki_storages_zfs.erl new file mode 100644 index 0000000..fc8ce00 --- /dev/null +++ b/apps/dreki/src/storages/dreki_storages_zfs.erl @@ -0,0 +1,2 @@ +-module(dreki_storages_zfs). + diff --git a/apps/dreki/src/dreki_dets_store.erl b/apps/dreki/src/store/dreki_dets_store.erl index 05cf9fb..aaa4c23 100644 --- a/apps/dreki/src/dreki_dets_store.erl +++ b/apps/dreki/src/store/dreki_dets_store.erl @@ -4,7 +4,7 @@ -behaviour(dreki_store_backend). --export([start/0, start/4, checkout/1, checkin/1, stop/0, stop/1]). +-export([start/0, start/5, checkout/1, checkin/1, stop/0, stop/1]). -export([valid_store/5]). -export([list/1, count/1, exists/2, get/2, create/2, update/2, delete/2]). @@ -14,7 +14,7 @@ start() -> ok. valid_store(_Namespace, _Location, _Name, _NSMod, _Args) -> ok. -start(Namespace, Name, _XUrn, Args) -> +start(Namespace, _NsMod, Name, _XUrn, Args) -> StoreName = {?MODULE, Namespace, Name}, File = maps:get(file_name, Args, <<Namespace/binary, ".", Name/binary, ".dets">>), FileName = binary:bin_to_list(File), @@ -71,4 +71,3 @@ update({dreki_dets_store_ref, Tab}, Task = #dreki_task{persisted=true, dirty=tru ok -> {ok, Task#dreki_task{dirty=false}}; {error, Error} -> {error, Error} end. - diff --git a/apps/dreki/src/store/dreki_mnesia_store.erl b/apps/dreki/src/store/dreki_mnesia_store.erl new file mode 100644 index 0000000..aceae09 --- /dev/null +++ b/apps/dreki/src/store/dreki_mnesia_store.erl @@ -0,0 +1,94 @@ +-module(dreki_mnesia_store). +-include("dreki.hrl"). +-include("dreki_otel.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). +-include_lib("kernel/include/logger.hrl"). +-behaviour(dreki_store_backend). + +-export([start/0, start/5, checkout/1, checkin/1, stop/0, stop/1]). +-export([valid_store/5]). +-export([list/1, count/1, exists/2, get/2, create/2, update/2, delete/2]). + +start() -> + ok. + +valid_store(_NS, _Loc, _Name, _NSMod, _Args) -> + ok. + +start(Namespace, NsMod, Name, _XUrn, Args) -> + TabName = binary_to_atom(iolist_to_binary(["dreki_mnesia_store", "-", Namespace, "-", Name])), + case lists:member(TabName, mnesia:system_info(tables)) of + true -> + {ok, TabName}; + false -> + DefaultOptions = [{rocksdb_copies, [node()]}], + Options0 = maps:get(mnesia_table_options, Args, DefaultOptions), + Options = [{attributes, NsMod:record_attributes()}, {record_name, NsMod:record_name()} | Options0], + {atomic, ok} = mnesia:create_table(TabName, Options), + ?LOG_NOTICE(#{message => "Created mnesia table for store", mnesia_table => TabName, store_namespace => Namespace, store_name => Name}), + {ok, TabName} + end. + +checkout(TabName) -> + {ok, TabName}. + +checkin(_) -> + ok. + +stop() -> + ok. + +stop(_) -> + ok. + +list(TabName) -> + transaction(fun () -> + [get(TabName, Id) || Id <- iter_all(mnesia:first(TabName), TabName)] + end). + +count(TabName) -> + mnesia:table_info(TabName, size). + +get(TabName, Id) -> + transaction(fun() -> + case mnesia:read(TabName, Id) of + [Tuple] -> + Tuple; + [] -> + not_found + end + end). + +exists(TabName, Id) -> + case get(TabName, Id) of + not_found -> + false; + _ -> + true + end. + +create(Table, Tuple) -> + transaction(fun() -> + mnesia:write(Table, Tuple, write), + get(Table, element(2, Tuple)) + end). + +update(Table, Tuple) -> + create(Table, Tuple). + +delete(TabName, Id) -> + transaction(fun() -> + mnesia:delete(TabName, Id, write) + end). + +transaction(Fun) -> + ?with_span(?FUN_NAME, #{}, fun(_) -> {atomic, Result} = mnesia:transaction(Fun), Result end). + +iter_all(Id, TabName) -> + ?with_span(?FUN_NAME, #{}, fun(_) -> iter_all(Id, TabName, []) end). + +iter_all('$end_of_table', _, Acc) -> + Acc; +iter_all(Id, TabName, Acc) -> + [Tuple] = mnesia:read(TabName, Id), + iter_all(mnesia:next(TabName, Id), TabName, [Tuple | Acc]). diff --git a/apps/dreki/src/dreki_store.erl b/apps/dreki/src/store/dreki_store.erl index ae6fd66..63d87a8 100644 --- a/apps/dreki/src/dreki_store.erl +++ b/apps/dreki/src/store/dreki_store.erl @@ -1,7 +1,7 @@ -module(dreki_store). -include("dreki.hrl"). -include("dreki_plum.hrl"). --include_lib("opentelemetry_api/include/otel_tracer.hrl"). +-include("dreki_otel.hrl"). -define(BACKENDS_PT, {dreki_stores, backends}). -compile({no_auto_import,[get/1]}). -export([backends/0]). @@ -16,13 +16,20 @@ -export([callback/3, list_/2, get_/2, new_/2, create_/3, update_/3, delete_/2]). --record(store, {urn, xurn, namespace, name, namespace_mod, backend_mod, backend_params}). +-record(store, {urn, xurn, namespace, name, namespace_mod, format, backend_mod, backend_params}). -type t() :: #store{}. -type store() :: t() | dreki_uri() | dreki_expanded_uri(). -backends() -> [dreki_dets_store, dreki_world_store]. -namespaces() -> [{<<"tasks">>, dreki_tasks, #{}}]. +backends() -> + [dreki_dets_store, dreki_world_store]. + +namespaces() -> + [ + {<<"tasks">>, dreki_tasks, #{}}, + {<<"requests">>, dreki_requests, #{}}, + {<<"storages">>, dreki_storages, #{}} + ]. namespace(Name) -> lists:keyfind(Name, 1, namespaces()). @@ -80,7 +87,7 @@ start_store__(Store) -> _Started -> {error, {store_already_started, Store#store.urn}} end. start_store___(Store = #store{backend_mod = Mod}) -> - case Mod:start(Store#store.namespace, Store#store.name, Store#store.urn, Store#store.backend_params) of + case Mod:start(Store#store.namespace, Store#store.namespace_mod, Store#store.name, Store#store.urn, Store#store.backend_params) of {ok, Backend} -> {ok, Backend}; {error, Error} -> {error, {store_start_failed, Store#store.urn, Error}} end. @@ -163,10 +170,11 @@ get_store(Location, Namespace, Name) -> end. as_record([Map]) -> as_record(Map); -as_record(#{urn := Urn, name := Name, namespace := Namespace, module := Module, module_params := ModuleParams}) -> +as_record(Store = #{urn := Urn, name := Name, namespace := Namespace, module := Module, module_params := ModuleParams}) -> {ok, XUrn} = dreki_urn:expand(Urn), {_, NSMod, _} = lists:keyfind(Namespace, 1, namespaces()), - #store{urn = Urn, xurn = XUrn, name = Name, namespace = Namespace, namespace_mod = NSMod, backend_mod = Module, backend_params = ModuleParams}. + Format = maps:get(format, Store, tuple), + #store{urn = Urn, xurn = XUrn, name = Name, namespace = Namespace, namespace_mod = NSMod, format = Format, backend_mod = Module, backend_params = ModuleParams}. store_as_map(#store{urn = Urn, name = Name, namespace = Namespace, namespace_mod = NSMod, backend_mod = Module, backend_params = ModuleParams}) -> #{urn => Urn, name => Name, namespace => Namespace, namespace_mod => NSMod, backend_mod => Module, backend_params => ModuleParams}. @@ -191,14 +199,14 @@ delete(SArg) -> get_store_(SArg, delete_, []). list_(#{resource := #{directory := _}}, Store) -> - handle_result(Store, collection, callback(Store, list, [])); + handle_result(Store, collection, callback(Store, list, []), list); list_(#{schema := _}, Store) -> {todo, schema}; list_(_, _) -> not_supported. get_(#{resource := #{resource := #{id := Id}}}, Store) -> - handle_result(Store, single, callback(Store, get, [Id])); + handle_result(Store, single, callback(Store, get, [Id]), get); get_(#{schema := _}, Store) -> {todo, get_schema}; get_(_, _) -> not_supported. @@ -208,21 +216,36 @@ new_(#{resource := #{directory := _}}, Store) -> New = NSMod:new(), {ok, New}. -create_(XUrn = #{directory := _}, Store, Data) -> - case validate(XUrn, Data) of - {ok, _} -> - handle_result(Store, single, callback(Store, create, [Data])); - {error, Errors} -> {error, #{code => validation_failed, status => 422, errors => jesse_error:to_json({error, Errors}, [])}} - end; +create_(XUrn = #{resource := #{directory := _}}, Store, Data0) -> + Id = maps:get(id, Data0, ksuid:gen_id()), + Data1 = Data0#{id => Id}, + case validate(XUrn, Data1) of + {ok, _} -> + Data = case Store#store.format of + tuple -> + Mod = Store#store.namespace_mod, + Mod:as_tuple(Data1); + map -> Data1 + end, + handle_result(Store, single, callback(Store, create, [Data]), create); + {error, Errors} -> + {error, #{code => validation_failed, status => 422, errors => jesse_error:to_json({error, Errors}, [])}} + end; create_(_, _, _) -> - not_supported. + not_supported. -update_(XUrn = #{resource := _}, Store, Data) -> +update_(XUrn = #{resource := #{resource := _}}, Store, Data0) -> case get(XUrn) of {ok, Prev} -> - case validate(XUrn, Data) of + case validate(XUrn, Data0) of {ok, _} -> - handle_result(Store, single, callback(Store, create, [Data])); + Data = case Store#store.format of + tuple -> + Mod = Store#store.namespace_mod, + Mod:as_tuple(Data0); + map -> Data0 + end, + handle_result(Store, single, callback(Store, update, [XUrn, Data]), update); {error, Errors} -> {error, #{code => validation_failed, status => 422, errors => jesse_error:to_json({error, Errors}, [])}} end; @@ -240,21 +263,27 @@ get_store_(StoreArg, Fun, Args) when is_binary(StoreArg) -> logger:debug("Expanding in get_store_ :) ~p", [StoreArg]), case dreki_urn:expand(StoreArg) of Error = {error, _} -> Error; - {ok, XUrn = #{resource := #{schemas := _}}} -> get_schema_(XUrn, Fun, Args); - {ok, XUrn = #{resource := #{schema := _}}} -> get_schema_(XUrn, Fun, Args); {ok, XUrn} -> get_store_(XUrn, Fun, Args) end; +get_store_(XUrn = #{resource := #{shemas := _}}, Fun, Args) -> + get_schema_(XUrn, Fun, Args); +get_store_(XUrn = #{resource := #{schema := _}}, Fun, Args) -> + get_schema_(XUrn, Fun, Args); get_store_(XUrn = #{resource := _}, Fun, Args) -> - ?with_span(<<"dreki_store:get_store_">>, #{}, fun(_Ctx) -> - logger:debug("Getting store usually! ~p", [XUrn]), - case get_store(XUrn) of + %%T = otel_tracer:start_span(opentelemetry:get_tracer(), <<"dreki_store">>, #{}), + %%?set_current_span(T), + ?with_span(opentelemetry:get_tracer(), #{}, fun(SpanCtx) -> + logger:debug("Getting store usually! ~p / Ctx: ~p / Span Ctx ~p / Cur Span Ctx ~p", [XUrn, otel_ctx:get_current(), SpanCtx, otel_tracer:current_span_ctx()]), + Result = case get_store(XUrn) of Error = {error, _} -> Error; {ok, Store} -> case apply(?MODULE, Fun, [XUrn, Store | Args]) of not_supported -> {error, {not_supported, Fun, maps:get(urn, XUrn)}}; Out -> Out end - end + end, + ?end_span(), + Result end). is_local(#store{xurn = #{kind := region}}) -> true; @@ -279,38 +308,50 @@ callback(true, #store{urn = Urn, backend_mod = Mod, backend_params = Params}, Fu Res end. -handle_result(_, _, {error, Err}) -> {error, Err}; -handle_result(Store, collection, {ok, Collection}) when is_list(Collection) -> - handle_result(Store, collection, Collection); -handle_result(Store, collection, Collection) when is_list(Collection) -> - {ok, handle_collection_result(Collection, Store, [])}; -handle_result(Store, single, {ok, Item}) when is_map(Item) -> - {ok, handle_single_result(Item, Store)}. - -handle_collection_result([Item | Rest], Store, Acc0) -> - Acc = case handle_single_result(Item, Store) of +handle_result(_, _, {error, Err}, _) -> {error, Err}; +handle_result(Store, collection, {ok, Collection}, PostCallback) when is_list(Collection) -> + handle_result(Store, collection, Collection, PostCallback); +handle_result(Store, collection, Collection, PostCallback) when is_list(Collection) -> + {ok, handle_collection_result(Collection, Store, [], PostCallback)}; +handle_result(Store, single, Item, PostCallback) -> + handle_single_result(Item, Store, PostCallback); +handle_result(Store, single, {ok, Item}, PostCallback)-> + handle_single_result(Item, Store, PostCallback). + +handle_collection_result([Item | Rest], Store, Acc0, PostCallback) -> + Acc = case handle_single_result(Item, Store, PostCallback) of ignore -> Acc0; {ok, I} -> [I | Acc0] end, - handle_collection_result(Rest, Store, Acc); -handle_collection_result([], Store, Acc) -> + handle_collection_result(Rest, Store, Acc, PostCallback); +handle_collection_result([], Store, Acc, _PostCallback) -> format_collection(Acc, Store). -handle_single_result(Item = #{id := LId}, Store = #store{namespace_mod = Mod}) -> +handle_single_result(Tuple, Store = #store{format = tuple, namespace_mod = Mod}, PostCallback) when is_tuple(Tuple) -> + handle_single_result(Mod:as_map(Tuple), Store, PostCallback); +handle_single_result(Item = #{id := LId}, Store = #store{namespace_mod = Mod}, PostCallback) -> case Mod:format_item(Item) of - ok -> format_item(Item, Store); - {ok, M} -> format_item(M, Store); + ok -> format_item(Item, Item, Store, PostCallback); + {ok, M} -> format_item(M, Item, Store, PostCallback); Err -> Err end. -format_item(Item = #{id := LId}, Store = #store{urn = Urn}) -> +format_item(Item = #{id := LId}, OriginalItem, Store = #store{urn = Urn, namespace = NS, namespace_mod = Mod}, PostCallback) -> + {ok, XUrn} = dreki_urn:expand(Urn), + SelfUrn = <<Urn/binary, ":", LId/binary>>, AtLinks = #{ - self => <<Urn/binary, ":", LId>>, - parent => maps:get(location, Urn) + self => SelfUrn, + parent => Urn }, AllAtLinks = maps:merge(AtLinks, maps:get('@links', Item, #{})), - I = #{'@id' => <<Urn/binary, ":", LId>>, '@links' => AllAtLinks}, - {ok, maps:merge(I, Item)}. + Actions = [#{id => Id, title => Title, new => New, create => Create} || {Id, Title, New, Create} <- Mod:actions(OriginalItem)], + I = #{'@id' => SelfUrn, '@ns' => NS, '@location' => maps:get(location, XUrn), '@links' => AllAtLinks, '@actions' => Actions}, + FullItem = maps:merge(I, Item), + case PostCallback of + create -> Mod:after_create(FullItem); + _ -> undefined + end, + {ok, FullItem}. format_collection(Data, Store = #store{urn = Urn}) -> #{'@links' => #{self => Urn}, @@ -362,11 +403,12 @@ get_schema_(#{urn := Urn, resource := #{namespace := NS, schema := #{schema := d case maps:get(default_version, Schema, not_found) of not_found -> not_found; SchemaVer -> - NewUrn = binary:replace(Urn, <<"schemas::">>, <<"schemas:", SchemaName/binary, ":", SchemaVer/binary>>), + NewUrn0 = binary:replace(Urn, <<"::schemas::">>, <<>>), + NewUrn = <<NewUrn0/binary, "::schemas:", SchemaName/binary, ":", SchemaVer/binary>>, {ok, XUrn} = dreki_urn:expand(NewUrn), - logger:debug("Looking up default schema as ~p", [NewUrn, XUrn]), + logger:debug("Looking up default schema as ~p (~p)", [NewUrn, XUrn]), get_schema_(XUrn, get_, Args) - end + end end; get_schema_(XUrn = #{resource := #{namespace := NS, schema := #{schema := SchemaName, version := Version}}}, get_, _) -> {_, NSMod, _} = namespace(NS), @@ -423,7 +465,11 @@ get_xurn_namespace(#{resource := #{namespace := NS}}) -> NS; get_xurn_namespace(#{resource := #{directory := #{namespace := NS}}}) -> NS; get_xurn_namespace(#{resource := #{resource := #{namespace := NS}}}) -> NS. +schema_loader(SchemaRef) -> + logger:debug("Trying to load schema ~p", [SchemaRef]), + get(SchemaRef). + validate_(XUrn, Schema, Data) -> JesseOpts = [{allowed_errors, infinity}, - {schema_loader_fun, fun get/1}], + {schema_loader_fun, fun schema_loader/1}], jesse:validate_with_schema(Schema, Data, JesseOpts). diff --git a/apps/dreki/src/dreki_store_backend.erl b/apps/dreki/src/store/dreki_store_backend.erl index 55db90e..55db90e 100644 --- a/apps/dreki/src/dreki_store_backend.erl +++ b/apps/dreki/src/store/dreki_store_backend.erl diff --git a/apps/dreki/src/dreki_store_namespace.erl b/apps/dreki/src/store/dreki_store_namespace.erl index 3095ef7..3095ef7 100644 --- a/apps/dreki/src/dreki_store_namespace.erl +++ b/apps/dreki/src/store/dreki_store_namespace.erl diff --git a/apps/dreki/src/dreki_task.erl b/apps/dreki/src/tasks/dreki_task.erl index b762aea..b762aea 100644 --- a/apps/dreki/src/dreki_task.erl +++ b/apps/dreki/src/tasks/dreki_task.erl diff --git a/apps/dreki/src/dreki_tasks.erl b/apps/dreki/src/tasks/dreki_tasks.erl index 0899386..750aed0 100644 --- a/apps/dreki/src/dreki_tasks.erl +++ b/apps/dreki/src/tasks/dreki_tasks.erl @@ -2,7 +2,7 @@ -include("dreki.hrl"). -behaviour(dreki_store_namespace). --export([start/0, version/0, valid_store/4, format_item/1, schemas/0, new/0]). +-export([start/0, version/0, valid_store/4, format_item/1, actions/0, actions/1, schemas/0, new/0, as_tuple/1, as_map/1]). %% old stuff -export([resolve/1, exists/1, read_uri/2]). @@ -13,14 +13,14 @@ valid_store(_Namespace, _Location, _Name, _BackendMod) -> ok. format_item(Item) -> ok. -handlers() -> [dreki_tasks_script, dreki_tasks_cloyster]. +handlers() -> [dreki_tasks_script, dreki_tasks_cloyster, drekid_function]. --record(?MODULE, { +-record(t, { id, version, schema, - handler, - handler_manifest + module, + params }). version() -> 1. @@ -29,13 +29,25 @@ new() -> #{ <<"@schema">> => <<"task:1.0">>, <<"id">> => ksuid:gen_id(), - <<"handler">> => <<"dreki_tasks_cloyster">>, - <<"handler_manifest">> => #{ + <<"module">> => <<"dreki_tasks_cloyster">>, + <<"params">> => #{ <<"@schema">> => <<"cloyster-task:1.0">>, <<"script">> => <<>> } }. +actions() -> + [ + {new, <<"New task">>, {dreki_tasks, new, []}, {dreki_store, create, []}} + ]. + +actions(_) -> + [ + {request, <<"Request run">>, + {dreki_requests, new, []}, + {dreki_store, create, []}} + ]. + schemas() -> Subs = lists:foldr(fun (Handler, Acc) -> maps:merge(Acc, Handler:schemas()) end, #{}, handlers()), @@ -80,6 +92,19 @@ schemas(task, <<"1.0">>) -> required => [handler, handler_manifest] }. +as_tuple(#{id := Id, module := Module, params := Params}) -> + #t{id = Id, version = undefined, schema = undefined, + module = Module, params = Params}. + +as_map(Task = #t{}) -> + #{ + id => Task#t.id, + version => Task#t.version, + schema => Task#t.schema, + module => Task#t.module, + params => Task#t.params + }. + %% old stuff read_uri(undefined, Uri) -> @@ -136,6 +161,6 @@ load_local_stores() -> env => Env, name => Name }, - maps:put(Name, Store, Acc) + maps:put(Name, Store, Acc) end, lists:foldr(MapFn, #{}, Val). diff --git a/apps/dreki/src/dreki_tasks_cloyster.erl b/apps/dreki/src/tasks/dreki_tasks_cloyster.erl index 3fb045d..3fb045d 100644 --- a/apps/dreki/src/dreki_tasks_cloyster.erl +++ b/apps/dreki/src/tasks/dreki_tasks_cloyster.erl diff --git a/apps/dreki/src/dreki_tasks_script.erl b/apps/dreki/src/tasks/dreki_tasks_script.erl index 8eeb563..8eeb563 100644 --- a/apps/dreki/src/dreki_tasks_script.erl +++ b/apps/dreki/src/tasks/dreki_tasks_script.erl diff --git a/apps/dreki/src/dreki_world.erl b/apps/dreki/src/world/dreki_world.erl index 437b6c8..437b6c8 100644 --- a/apps/dreki/src/dreki_world.erl +++ b/apps/dreki/src/world/dreki_world.erl diff --git a/apps/dreki/src/dreki_world_dns.erl b/apps/dreki/src/world/dreki_world_dns.erl index 058c9db..058c9db 100644 --- a/apps/dreki/src/dreki_world_dns.erl +++ b/apps/dreki/src/world/dreki_world_dns.erl diff --git a/apps/dreki/src/dreki_world_plum_events.erl b/apps/dreki/src/world/dreki_world_plum_events.erl index b32ae09..b32ae09 100644 --- a/apps/dreki/src/dreki_world_plum_events.erl +++ b/apps/dreki/src/world/dreki_world_plum_events.erl diff --git a/apps/dreki/src/dreki_world_server.erl b/apps/dreki/src/world/dreki_world_server.erl index 2bc41ed..2bc41ed 100644 --- a/apps/dreki/src/dreki_world_server.erl +++ b/apps/dreki/src/world/dreki_world_server.erl diff --git a/apps/dreki/src/dreki_world_store.erl b/apps/dreki/src/world/dreki_world_store.erl index 71ef2ce..050efd0 100644 --- a/apps/dreki/src/dreki_world_store.erl +++ b/apps/dreki/src/world/dreki_world_store.erl @@ -8,7 +8,7 @@ -type db() :: #store{}. -type args() :: #{}. --export([start/0, start/4, checkout/1, checkin/1, stop/0, stop/1]). +-export([start/0, start/5, checkout/1, checkin/1, stop/0, stop/1]). -export([valid_store/5]). -export([list/1, count/1, exists/2, get/2, create/2, update/2, delete/2]). @@ -19,7 +19,7 @@ valid_store(_Namespace, Location, _Name, _NSMod, _Args) -> end. start() -> ok. -start(_, _, _, _) -> {ok, dreki_world_store}. +start(_, _, _, _, _) -> {ok, dreki_world_store}. checkout(_) -> {ok, #store{}}. checkin(_) -> ok. diff --git a/apps/dreki/src/dreki_world_tasks.erl b/apps/dreki/src/world/dreki_world_tasks.erl index c27a7dc..c27a7dc 100644 --- a/apps/dreki/src/dreki_world_tasks.erl +++ b/apps/dreki/src/world/dreki_world_tasks.erl diff --git a/apps/dreki_web/.gitignore b/apps/dreki_web/.gitignore index 8dec94f..8946870 100644 --- a/apps/dreki_web/.gitignore +++ b/apps/dreki_web/.gitignore @@ -18,3 +18,4 @@ _build rebar3.crashdump *~ /priv/static/ +/assets/node_modules/ diff --git a/apps/dreki_web/assets/package-lock.json b/apps/dreki_web/assets/package-lock.json index c99c8b3..b6aa028 100644 --- a/apps/dreki_web/assets/package-lock.json +++ b/apps/dreki_web/assets/package-lock.json @@ -12,7 +12,8 @@ "@tailwindcss/forms": "^0.5.0", "@tailwindcss/line-clamp": "^0.3.1", "@tailwindcss/typography": "^0.5.2", - "crossfilter": "^1.3.12" + "crossfilter": "^1.3.12", + "xterm": "^4.18.0" }, "devDependencies": { "@hotwired/stimulus": "^3.0.1", @@ -2063,6 +2064,11 @@ "node": ">=0.4" } }, + "node_modules/xterm": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.18.0.tgz", + "integrity": "sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ==" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -3567,6 +3573,11 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, + "xterm": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.18.0.tgz", + "integrity": "sha512-JQoc1S0dti6SQfI0bK1AZvGnAxH4MVw45ZPFSO6FHTInAiau3Ix77fSxNx3mX4eh9OL4AYa8+4C8f5UvnSfppQ==" + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/apps/dreki_web/assets/package.json b/apps/dreki_web/assets/package.json index 07072c5..1aa07ff 100644 --- a/apps/dreki_web/assets/package.json +++ b/apps/dreki_web/assets/package.json @@ -27,6 +27,7 @@ "@tailwindcss/forms": "^0.5.0", "@tailwindcss/line-clamp": "^0.3.1", "@tailwindcss/typography": "^0.5.2", - "crossfilter": "^1.3.12" + "crossfilter": "^1.3.12", + "xterm": "^4.18.0" } } diff --git a/apps/dreki_web/rebar.config b/apps/dreki_web/rebar.config index b16060a..e439af6 100644 --- a/apps/dreki_web/rebar.config +++ b/apps/dreki_web/rebar.config @@ -5,7 +5,8 @@ {erlydtl, "0.14.0"}, {oauth2c, {git, "https://github.com/kivra/oauth2_client", {branch, "master"}}}, {cowboy_telemetry, "~> 0.4.0"}, - {opentelemetry_cowboy, "~> 0.1.0"} + {opentelemetry_cowboy, "~> 0.1.0"}, + {prometheus_cowboy, "0.1.8"} ]}. {plugins, [ diff --git a/apps/dreki_web/src/dreki_web_admin_tasks.erl b/apps/dreki_web/src/admin/dreki_web_admin_tasks.erl index adc2689..adc2689 100644 --- a/apps/dreki_web/src/dreki_web_admin_tasks.erl +++ b/apps/dreki_web/src/admin/dreki_web_admin_tasks.erl diff --git a/apps/dreki_web/src/dreki_web_admin_world.erl b/apps/dreki_web/src/admin/dreki_web_admin_world.erl index 1ceaaee..1ceaaee 100644 --- a/apps/dreki_web/src/dreki_web_admin_world.erl +++ b/apps/dreki_web/src/admin/dreki_web_admin_world.erl diff --git a/apps/dreki_web/src/dreki_web_index.erl b/apps/dreki_web/src/api/dreki_web_index.erl index 2ed8a38..2ed8a38 100644 --- a/apps/dreki_web/src/dreki_web_index.erl +++ b/apps/dreki_web/src/api/dreki_web_index.erl diff --git a/apps/dreki_web/src/dreki_web_task.erl b/apps/dreki_web/src/api/dreki_web_task.erl index fdcf9cd..fdcf9cd 100644 --- a/apps/dreki_web/src/dreki_web_task.erl +++ b/apps/dreki_web/src/api/dreki_web_task.erl diff --git a/apps/dreki_web/src/cowboy_access_log_h.erl b/apps/dreki_web/src/cowboy_access_log_h.erl new file mode 100644 index 0000000..433d706 --- /dev/null +++ b/apps/dreki_web/src/cowboy_access_log_h.erl @@ -0,0 +1,238 @@ +-module(cowboy_access_log_h). +-behaviour(cowboy_stream). + +-dialyzer(no_undefined_callbacks). + +-type extra_info_fun() :: fun((cowboy_req:req()) -> #{atom() => term()}). +-export_type([extra_info_fun/0]). + +%% API exports + +-export([set_extra_info_fun/2]). + +%% callback exports + +-export([init/3]). +-export([data/4]). +-export([info/3]). +-export([terminate/3]). +-export([early_error/5]). + +-type state() :: #{ + next := any(), + req := cowboy_req:req(), + meta := #{started_at => genlib_time:ts()}, + ext_fun := extra_info_fun() +}. + +%% API + +-spec set_extra_info_fun(extra_info_fun(), cowboy:opts()) + -> cowboy:opts(). +set_extra_info_fun(Fun, Opts) when is_function(Fun, 1) -> + Opts#{extra_info_fun => Fun}. + +%% callbacks + +-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) + -> {cowboy_stream:commands(), state()}. +init(StreamID, Req, Opts) -> + State = make_state(Req, Opts), + {Commands0, Next} = cowboy_stream:init(StreamID, Req, Opts), + {Commands0, State#{next => Next}}. + +-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State) + -> {cowboy_stream:commands(), State} when State::state(). +data(StreamID, IsFin, Data, #{next := Next0} = State) -> + {Commands0, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0), + {Commands0, State#{next => Next}}. + +-spec info(cowboy_stream:streamid(), any(), State) + -> {cowboy_stream:commands(), State} when State::state(). +info(StreamID, {IsResponse, Code, Headers, _} = Info, #{req := Req, next := Next0} = State) when + IsResponse == response; + IsResponse == error_response +-> + _ = log_access_safe(Code, Headers, State, get_request_body_length(Req)), + {Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0), + {Commands0, State#{next => Next}}; +info(StreamID, Info, #{next := Next0} = State) -> + {Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0), + {Commands0, State#{next => Next}}. + +-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), state()) -> any(). +terminate(StreamID, Reason, #{next := Next}) -> + cowboy_stream:terminate(StreamID, Reason, Next). + +-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(), + cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp + when Resp::cowboy_stream:resp_command(). + +%% NOTE: in early_error cowboy uses PartialReq, a cowboy_req:req() - like structure +%% for more info see https://ninenines.eu/docs/en/cowboy/2.7/manual/cowboy_stream/#_callbacks + +early_error(StreamID, Reason, PartialReq, {_, Code, Headers, _} = Resp, Opts) -> + State = make_state(PartialReq, Opts), + _ = log_access_safe(Code, Headers, State, undefined), + cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, State). + +%% private functions + +log_access_safe(Code, Headers, #{req := Req} = State, ReqBodyLength) -> + try + logger:log(info, prepare_meta(Code, Headers, State, ReqBodyLength)), + Req + catch + Class:Reason:Stacktrace -> + Stack = genlib_format:format_stacktrace(Stacktrace, [newlines]), + _ = logger:error( + "Log access failed for: [~p, ~p, ~p]~nwith: ~p:~p~nstacktrace: ~ts", + [Code, Headers, Req, Class, Reason, Stack] + ), + Req + end. + +get_process_meta() -> + case logger:get_process_metadata() of + undefined -> + #{}; + Meta -> + Meta + end. + +% domain field specifies the functional area that send log event +% as we want to save logs from this app in a separate file, +% we can easily filter logs by their domain using OTP filter functions. +prepare_meta(Code, Headers, #{req := Req, meta:= Meta0, ext_fun := F}, ReqBodyLength) -> + AccessMeta = genlib_map:compact(#{ + domain => [cowboy_access_log], + status => Code, + remote_addr => get_remote_addr(Req), + peer_addr => get_peer_addr(Req), + request_method => cowboy_req:method(Req), + request_path => cowboy_req:path(Req), + request_length => ReqBodyLength, + response_length => get_response_len(Headers), + request_time => get_request_duration(Meta0), + trace_id => maps:get(<<"trace-id">>, Req, <<"no-trace">>), + 'http_x-request-id' => cowboy_req:header(<<"x-request-id">>, Req, undefined) + }), + AccessMeta1 = maps:merge(get_process_meta(), AccessMeta), + maps:merge(F(Req), AccessMeta1). + +get_request_body_length(Req) -> + case cowboy_req:has_body(Req) of + false -> undefined; + true -> cowboy_req:body_length(Req) + end. + +get_peer_addr(Req) -> + {IP, _Port} = cowboy_req:peer(Req), + genlib:to_binary(inet:ntoa(IP)). + +get_remote_addr(Req) -> + case determine_remote_addr(Req) of + {ok, RemoteAddr} -> + genlib:to_binary(inet:ntoa(RemoteAddr)); + _ -> + undefined + end. + +determine_remote_addr(Req) -> + Peer = cowboy_req:peer(Req), + Value = cowboy_req:header(<<"x-forwarded-for">>, Req), + determine_remote_addr_from_header(Value, Peer). + +determine_remote_addr_from_header(undefined, {IP, _Port}) -> + % undefined, assuming no proxies were involved + {ok, IP}; +determine_remote_addr_from_header(Value, _Peer) when is_binary(Value) -> + ClientPeer = string:strip(binary_to_list(Value)), + case string:tokens(ClientPeer, ", ") of + [ClientIP | _Proxies] -> + inet:parse_strict_address(ClientIP); + _ -> + {error, malformed} + end. + +get_request_duration(Meta) -> + case maps:get(started_at, Meta, undefined) of + undefined -> + undefined; + StartTime -> + (genlib_time:ticks() - StartTime) / 1000000 + end. + +get_response_len(Headers) -> + case maps:get(<<"content-length">>, Headers, undefined) of + undefined -> + undefined; + Len -> + genlib:to_int(Len) + end. + +make_state(Req, Opts) -> + ExtFun = make_ext_fun(Opts), + set_meta(#{req => Req, ext_fun => ExtFun}). + +set_meta(State) -> + State#{meta => #{started_at => genlib_time:ticks()}}. + +make_ext_fun(Opts) -> + maps:get(extra_info_fun, Opts, fun(_Req) -> #{} end). + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec filter_meta_test() -> _. +filter_meta_test() -> + Req = #{ + pid => self(), + peer => {{42, 42, 42, 42}, 4242}, + method => <<"GET">>, + path => <<>>, + qs => <<>>, + version => 'HTTP/1.1', + headers => #{}, + host => <<>>, + port => undefined, + has_body => true + }, + State = make_state(Req, #{}), + #{ + request_method := <<"GET">>, + request_path := <<>>, + request_time := _, + response_length := 33, + request_length := 100, + peer_addr := <<"42.42.42.42">>, + status := 200 + } = prepare_meta(200, #{<<"content-length">> => <<"33">>}, State, 100). + +-spec filter_meta_for_error_test() -> _. +filter_meta_for_error_test() -> + Req = #{ + pid => self(), + peer => {{42, 42, 42, 42}, 4242}, + method => <<"GET">>, + path => <<>>, + qs => <<>>, + version => 'HTTP/1.1', + headers => #{}, + host => <<>>, + port => undefined, + has_body => true + }, + State = make_state(Req, #{}), + #{ + peer_addr := <<"42.42.42.42">>, + remote_addr := <<"42.42.42.42">>, + request_method := <<"GET">>, + request_path := <<>>, + request_time := _, + status := 400 + } = prepare_meta(400, #{}, State, undefined). + +-endif. diff --git a/apps/dreki_web/src/dreki_web.app.src b/apps/dreki_web/src/dreki_web.app.src index cf731fc..4122e3b 100644 --- a/apps/dreki_web/src/dreki_web.app.src +++ b/apps/dreki_web/src/dreki_web.app.src @@ -10,7 +10,8 @@ cowboy, trails, cowboy_telemetry, - opentelemetry_cowboy + opentelemetry_cowboy, + prometheus_cowboy ]}, {env,[]}, {modules, []}, diff --git a/apps/dreki_web/src/dreki_web_app.erl b/apps/dreki_web/src/dreki_web_app.erl index 5b6454e..5b3c1a0 100644 --- a/apps/dreki_web/src/dreki_web_app.erl +++ b/apps/dreki_web/src/dreki_web_app.erl @@ -13,8 +13,12 @@ start(_StartType, _StartArgs) -> Config = application:get_all_env(dreki_web), Transport = proplists:get_value(transport, Config), CowboyEnv = #{ - middlewares => [dreki_web_auth, cowboy_router, cowboy_handler], - stream_handlers => [cowboy_telemetry_h, cowboy_stream_h], + middlewares => [dreki_web_auth, cowboy_router, dreki_web_handler], + stream_handlers => [cowboy_telemetry_h, + cowboy_access_log_h, + cowboy_metrics_h, + cowboy_stream_h], + metrics_callback => fun prometheus_cowboy2_instrumenter:observe/1, env => #{ dispatch => routes() } @@ -35,7 +39,8 @@ routes() -> Trails = [ {"/", dreki_web_index, undefined}, {"/static/[...]", cowboy_static, - {priv_dir, dreki_web, "static", [{mimetypes, dreki_web, detect_web_mimetype}]}}, + {priv_dir, dreki_web, "static", [{mimetypes, dreki_web, detect_web_mimetype}]}}, + {"/metrics/[:registry]", prometheus_cowboy2_handler, []}, %% API {"/api/tasks/:id", dreki_web_task, undefined}, @@ -59,6 +64,7 @@ routes() -> {"/admin/:location/:namespace/:directory", dreki_web_ui_stores, undefined}, {"/admin/:location/:namespace/:directory/_/:action", dreki_web_ui_stores, action}, {"/admin/:location/:namespace/:directory/:id", dreki_web_ui_stores, undefined}, + {"/admin/:location/:namespace/:directory/:id/_/:action", dreki_web_ui_stores, action}, {"/admin/[...]", dreki_web_ui_error, #{code => 404, status => <<"Not found">>}}, diff --git a/apps/dreki_web/src/dreki_web_handler.erl b/apps/dreki_web/src/dreki_web_handler.erl new file mode 100644 index 0000000..a30fd1d --- /dev/null +++ b/apps/dreki_web/src/dreki_web_handler.erl @@ -0,0 +1,99 @@ +%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu> +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% Handler middleware. +%% +%% Execute the handler given by the <em>handler</em> and <em>handler_opts</em> +%% environment values. The result of this execution is added to the +%% environment under the <em>result</em> value. +-module(dreki_web_handler). +-include_lib("opentelemetry_api/include/otel_tracer.hrl"). +-behaviour(cowboy_middleware). + +-export([execute/2]). +-export([terminate/4]). + +-callback init(Req, any()) + -> {ok | module(), Req, any()} + | {module(), Req, any(), any()} + when Req::cowboy_req:req(). + +-callback terminate(any(), map(), any()) -> ok. +-optional_callbacks([terminate/3]). + +-spec execute(Req, Env) -> {ok, Req, Env} + when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +execute(Req, Env=#{handler := Handler}) -> + Headers = maps:get(headers, Req), + otel_propagator_text_map:extract(maps:to_list(Headers)), + HandlerB = atom_to_binary(Handler), + Method = maps:get(method, Req), + Attributes = [ + {'http.host', maps:get(host, Req)}, + {'http.host.port', maps:get(port, Req)}, + {'http.method', Method}, + {'http.scheme', maps:get(scheme, Req)}, + {'http.target', maps:get(path, Req)}, + {'http.user_agent', maps:get(<<"user-agent">>, Headers, <<"">>)} + ], + SpanName = iolist_to_binary([<<"HTTP ">>, Method, <<" ">>, HandlerB]), + logger:debug("SpanName: ~p", [SpanName]), + ?with_span(SpanName, #{}, fun(Ctx) -> execute_(Req, Env, Ctx) end). + +execute_(Req0, Env=#{handler := Handler, handler_opts := HandlerOpts}, Ctx) -> + TraceId = otel_span:hex_trace_id(otel_tracer:current_span_ctx()), + Req = Req0#{<<"trace-id">> => TraceId}, + try Handler:init(Req, HandlerOpts) of + {ok, Req2, State} -> + Headers0 = maps:get(headers, Req2), + Headers = otel_propagator_text_map:inject(Headers0, + fun(Headers, Key, Value) -> + maps:put(Key, Value, Headers) + end), + Req3 = maps:put(headers, Headers, Req2), + Result = terminate(normal, Req3, State, Handler), + {ok, Req3, Env#{result => Result, trace_id => TraceId}}; + {Mod, Req2, State} -> + ?add_event(<<"HTTP_UPGRADE">>, #{module => Mod}), + Mod:upgrade(Req2, Env, Handler, State); + {Mod, Req2, State, Opts} -> + ?add_event(<<"HTTP_UPGRADE">>, #{module => Mod}), + Mod:upgrade(Req2, Env, Handler, State, Opts) + catch Class:Reason:Stacktrace -> + ?set_status(error, iolist_to_binary([atom_to_binary(Class)])), + render_error(Class, Reason, Stacktrace, Handler, Req, Env, Ctx) + end. + +render_error(Class, Reason, Stacktrace, Handler, Req, Env, Ctx) -> + ReasonS = lists:flatten(io_lib:format("~p", [Reason])), + Assigns = [ + {dreki_node, node()}, + {site_title, "Dreki"}, + {class, atom_to_binary(Class)}, + {reason, ReasonS}, + {stacktrace, [lists:flatten(io_lib:format("~p", [Line])) || Line <- Stacktrace]}, + {trace_id, maps:get(<<"trace-id">>, Req, <<"no-trace">>)} + ], + logger:error(#{app => dreki_web, handler => Handler, error => {Class, Reason, Stacktrace}}), + {ok, Html} = crash_dtl:render(Assigns), + cowboy_req:reply(500, #{<<"content-type">> => <<"text/html">>}, Html, Req). + +-spec terminate(any(), Req | undefined, any(), module()) -> ok when Req::cowboy_req:req(). +terminate(Reason, Req, State, Handler) -> + case erlang:function_exported(Handler, terminate, 3) of + true -> + Handler:terminate(Reason, Req, State); + false -> + ok + end. diff --git a/apps/dreki_web/src/dreki_web_ui.erl b/apps/dreki_web/src/ui/dreki_web_ui.erl index d60cc6e..e394632 100644 --- a/apps/dreki_web/src/dreki_web_ui.erl +++ b/apps/dreki_web/src/ui/dreki_web_ui.erl @@ -24,7 +24,8 @@ assigns(Req, Assigns0) -> {"identity", maps:get(identity, Req)}, {"identity_name", dreki_web:identity_name(Req)}, {"dreki_node", node()}, - {"dreki_world", dreki_world:to_map()} + {"dreki_world", dreki_world:to_map()}, + {"trace_id", maps:get(<<"trace-id">>, Req, <<"no-trace">>)} | Assigns]. content_types_accepted(Req, State) -> diff --git a/apps/dreki_web/src/dreki_web_ui_error.erl b/apps/dreki_web/src/ui/dreki_web_ui_error.erl index ccda150..ccda150 100644 --- a/apps/dreki_web/src/dreki_web_ui_error.erl +++ b/apps/dreki_web/src/ui/dreki_web_ui_error.erl diff --git a/apps/dreki_web/src/dreki_web_ui_index.erl b/apps/dreki_web/src/ui/dreki_web_ui_index.erl index 9f4684e..9f4684e 100644 --- a/apps/dreki_web/src/dreki_web_ui_index.erl +++ b/apps/dreki_web/src/ui/dreki_web_ui_index.erl diff --git a/apps/dreki_web/src/dreki_web_ui_json_form.erl b/apps/dreki_web/src/ui/dreki_web_ui_json_form.erl index 49f69f8..51a9de4 100644 --- a/apps/dreki_web/src/dreki_web_ui_json_form.erl +++ b/apps/dreki_web/src/ui/dreki_web_ui_json_form.erl @@ -28,6 +28,8 @@ to_html([{Node, Attrs, Content} | Rest], Acc) -> AttrsS = attrs_to_html(Attrs), logger:debug("Node ~p Attrs ~p Content ~p", [Node, Attrs, Content]), to_html(Rest, [[<<"<">>, BNode, <<" ">>, AttrsS, <<">">>, to_html(Content), <<"</">>, BNode, <<">">>] | Acc]); +to_html([List | Rest], Acc) when is_list(List) -> + to_html(Rest, [to_html(List) | Acc]); to_html([], Acc) -> lists:reverse(Acc). @@ -45,13 +47,27 @@ attrs_to_html([{Attr, Value} | Rest], Acc) -> attrs_to_html([], Acc) -> Acc. +put_new(Key, Value, Map) -> + case maps:is_key(Key, Map) of + true -> + Map; + false -> + maps:put(Key, Value, Map) + end. + render(Schema, Opts) -> + render(Schema, Opts, {'div', [{class, <<"mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">>}]}). + +render(Schema, Opts, {Elem, Attrs}) -> + [{Elem, Attrs, render(Schema, Opts, undefined)}]; +render(Schema, Opts0, undefined) -> + Opts = put_new(name, <<"form">>, Opts0), lists:reverse(maps:fold(fun (Key, Value, Acc) -> - case render_property(Key, Value, Schema, Schema, Opts) of - {ok, Html} -> [Html | Acc]; - ignore -> Acc - end - end, [], maps:get(properties, Schema))). + case render_property(Key, Value, Schema, Schema, Opts) of + {ok, Html} -> [Html | Acc]; + ignore -> Acc + end + end, [], maps:get(properties, Schema))). render_property(Field, Config = #{enum := Enum}, Parent, Schema, Opts) -> FormAttrs = maps:get(<<"dreki:form">>, Config, #{}), @@ -69,6 +85,19 @@ render_property(Field, Config = #{type := Type}, Parent, Schema, Opts) -> InputOpts = #{required => Required, label => Label, input_type => InputType}, {ok, render_input(Input, Field, Type, InputOpts, Opts)}; +render_property(Key, SubSchema = #{<<"@schema">> := SubSchemaUrn}, Parent, Schema, Opts) -> + Name = maps:get(name, Opts), + KeyB = atom_to_binary(Key), + InnerForm = render(SubSchema, Opts#{name => <<Name/binary, "[", KeyB/binary, "]">>}, undefined), + Head = {'div', [{class, <<"sm:col-span-6 mb-3">>}], [ + {h3, [{class, <<"text-lg leading-6 font-medium text-gray-900 dark:text-gray-200">>}], maps:get(title, SubSchema, SubSchemaUrn)}, + {p, [{class, <<"mt-1 text-sm text-gray-500 dark:text-gray-400">>}], maps:get(description, SubSchema, <<>>)} + ]}, + {ok, {'div', [{class, <<"sm:col-span-6">>}], + [ + {'div', [{class, <<"mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">>}], [Head, InnerForm]} + ]}}; + render_property(Field, #{'$ref' := Ref}, Parent, Schema = #{'$defs' := Defs}, Opts) -> case maps:get(Ref, Defs, undefined) of undefined -> logger:error("didn't get ref ~p", [Ref]); @@ -88,26 +117,30 @@ base_attributes(Field, IOpts, FOpts) -> render_input(select, Field, _, IOpts, FOpts) -> {Name, Attributes} = base_attributes(Field, IOpts, FOpts), OptionsHtml = lists:map(fun (Opt) -> {option, [], Opt} end, maps:get(values, IOpts, [])), - {'div', [{class, <<"json-field">>}], [ - {label, [{for, Name}], maps:get(label, IOpts, Field)}, + {'div', [{class, <<"json-field">>}], + [ + label(Field, Name, maps:get(label, IOpts, Field)), {select, Attributes, OptionsHtml} - ]}; + ]}; render_input(input, Field, Type, IOpts, FOpts) -> - {Name, Attributes0} = base_attributes(Field, IOpts, FOpts), - Attributes = [ - {value, maps:get(value, IOpts, undefined)}, - {placeholder, maps:get(placeholder, IOpts, undefined)}, - {readonly, maps:get(readonly, IOpts, false)}, - {autocomplete, maps:get(autocomplete, IOpts, <<"off">>)}, - {type, maps:get(input_type, IOpts)}, - {class, <<"shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">>} - | Attributes0], - HtmlNode = {'div', [{class, <<"json-field">>}], [ - {label, [{for, Name}, {class, <<"block text-sm font-medium text-gray-700">>}], maps:get(label, IOpts, Field)}, - {'div', [{class, <<"mt-1">>}], [{input, Attributes}]} - ]}, - HtmlNode. + {Name, Attributes0} = base_attributes(Field, IOpts, FOpts), + Attributes = [ + {value, maps:get(value, IOpts, undefined)}, + {placeholder, maps:get(placeholder, IOpts, undefined)}, + {readonly, maps:get(readonly, IOpts, false)}, + {autocomplete, maps:get(autocomplete, IOpts, <<"off">>)}, + {type, maps:get(input_type, IOpts)}, + {class, <<"shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md dark:bg-gray-800">>} + | Attributes0], + {'div', [{class, <<"json-field sm:col-span-6">>}], + [ + label(Field, Name, maps:get(label, IOpts, Field)), + {'div', [{class, <<"mt-1">>}], [{input, Attributes}]} + ]}. + +label(_Field, Name, Label) -> + {label, [{for, Name}, {class, <<"block text-sm font-medium dark:text-gray-400 text-gray-700 dark:text-gray-400">>}], Label}. as_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom); @@ -119,7 +152,7 @@ input_type(_) -> <<"text">>. field_name(Field, Opts) -> FB = as_binary(Field), BaseName = maps:get(name, Opts, <<"form">>), - <<"[", BaseName/binary, "]", FB/binary>>. + <<BaseName/binary, "[", FB/binary, "]">>. field_id(Field, Opts) -> FB = as_binary(Field), diff --git a/apps/dreki_web/src/dreki_web_ui_node.erl b/apps/dreki_web/src/ui/dreki_web_ui_node.erl index fdb7c77..fdb7c77 100644 --- a/apps/dreki_web/src/dreki_web_ui_node.erl +++ b/apps/dreki_web/src/ui/dreki_web_ui_node.erl diff --git a/apps/dreki_web/src/dreki_web_ui_stores.erl b/apps/dreki_web/src/ui/dreki_web_ui_stores.erl index d39e571..e39cd48 100644 --- a/apps/dreki_web/src/dreki_web_ui_stores.erl +++ b/apps/dreki_web/src/ui/dreki_web_ui_stores.erl @@ -63,13 +63,17 @@ request(<<"GET">>, undefined, #{resource := #{namespace := NS}}, Req) -> %% List request(<<"GET">>, undefined, Urn = #{location := Loc, resource := #{directory := #{directory := Dir, namespace := NS}}}, Req) -> {ok, Result} = dreki_store:list(Urn), - Results = lists:map(fun(Result) -> + Results0 = lists:map(fun(Result) -> Result#{href => urn_to_path(maps:get('@id', Result))} end, maps:get(data, Result)), + Results = deep_map_to_list(Results0), Html = dreki_web_ui:render(Req, store_list_dtl, [ - {location, Loc}, {namespace, NS}, {directory, Dir}, {results, Results}, - {new, href(Req, <<"_/new">>)} - ]), + {location, Loc}, {namespace, NS}, {directory, Dir}, + {results, Results}, + {actions, [ + [{title, "New"}, {href, href(Req, <<"_/new">>)}] + ]} + ]), {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined}; %% New @@ -90,8 +94,54 @@ request(<<"GET">>, <<"new">>, #{urn := Urn, location := Loc, resource := #{direc request(<<"GET">>, undefined, Urn = #{location := Loc, resource := #{resource := #{id := Id, directory := Dir, namespace := NS}}}, Req) -> {ok, Result0} = dreki_store:get(Urn), Result = Result0#{'@href' => urn_to_path(Urn)}, - Html = dreki_web_ui:render(Req, store_show_dtl, [{location, Loc}, {id, Id}, {directory, Dir}, {namespace, NS}, {result, Result}]), - {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined}. + Actions = lists:foldr(fun (#{id := Id, title := Title}, Acc) -> + IdB = atom_to_binary(Id), + Action = [{id, Id}, {title, Title}, {href, href(Req, <<"_/", IdB/binary>>)}], + [Action | Acc] + end, [], maps:get('@actions', Result)), + Html = dreki_web_ui:render(Req, store_show_dtl, [{location, Loc}, {id, Id}, {directory, Dir}, {namespace, NS}, + {result, deep_map_to_list(Result)}, + {actions, [ + [{title, "Edit (UI)"}, {href, href(Req, <<"_/edit">>)}] + | Actions + ]} + ]), + {ok, dreki_web_ui:reply_html(Req, 200, Html), undefined}; + +%% Show action +request(<<"GET">>, Action0, Urn = #{location := Loc, resource := #{resource := #{id := Id, directory := Dir, namespace := NS}}}, Req) -> + {ok, Result0} = dreki_store:get(Urn), + Result = Result0#{'@href' => urn_to_path(Urn)}, + Action = binary_to_existing_atom(Action0), + Path = href(Req, <<"_/", Action0/binary>>), + Act = lists:filter(fun (A) -> maps:get(id, A) =:= Action end, maps:get('@actions', Result)), + case Act of + [#{title := Title, new := {Mod, Fun, Args}}] -> + {ok, Schema} = apply(Mod, Fun, [Result | Args]), + Form = dreki_web_ui_json_form:render_html(Schema, #{}), + Html = dreki_web_ui:render(Req, store_new_dtl, [ + {location, Loc}, {id, Id}, {directory, Dir}, {namespace, NS}, + {method, <<"POST">>}, + {title, Title}, + {result, deep_map_to_list(Result)}, + {target, Path}, + {form, Form} + ]), + {dreki_web_ui:reply_html(Req, 200, Html), undefined}; + _ -> + dreki_web_ui_error:init(Req, #{code => 404, status => "Not Found", message => "No such action"}) + end; +request(<<"POST">>, Action0, Urn = #{location := Loc, resource := #{resource := #{id := Id, directory := Dir, namespace := NS}}}, Req) -> + {ok, Result0} = dreki_store:get(Urn), + Result = Result0#{'@href' => urn_to_path(Urn)}, + Action = binary_to_existing_atom(Action0), + Act = lists:filter(fun (A) -> maps:get(id, A) =:= Action end, maps:get('@actions', Result)), + case Act of + [#{title := Title, new := {Mod, Fun, Args}}] -> + Req; + _ -> + dreki_web_ui_error:init(Req, #{code => 404, status => "Not Found", message => "No such action"}) + end. derpinit(Req, _) -> Json = #{<<"error">> => false, <<"service">> => <<"dreki">>}, @@ -112,6 +162,19 @@ location_to_path(Location) -> false -> binary:replace(Location, <<Root/binary, ":">>, <<"/admin/">>) end. +deep_map_to_list(List) when is_list(List) -> + [deep_map_to_list(Map) || Map <- List]; +deep_map_to_list(Map) when is_map(Map) -> + deep_map_to_list(maps:to_list(Map), []). + +deep_map_to_list([{Key, Map} | Rest], Acc) when is_map(Map) -> + List = deep_map_to_list(Map), + deep_map_to_list(Rest, [{Key, List} | Acc]); +deep_map_to_list([Item | Rest], Acc) -> + deep_map_to_list(Rest, [Item | Acc]); +deep_map_to_list([], Acc) -> + Acc. + urn_to_path(Urn) when is_binary(Urn) -> {ok, XUrn} = dreki_urn:expand(Urn), urn_to_path(XUrn); diff --git a/apps/dreki_web/src/dreki_web_ui_task.erl b/apps/dreki_web/src/ui/dreki_web_ui_task.erl index 1ced583..1ced583 100644 --- a/apps/dreki_web/src/dreki_web_ui_task.erl +++ b/apps/dreki_web/src/ui/dreki_web_ui_task.erl diff --git a/apps/dreki_web/src/dreki_web_ui_tasks.erl b/apps/dreki_web/src/ui/dreki_web_ui_tasks.erl index e9748b8..e9748b8 100644 --- a/apps/dreki_web/src/dreki_web_ui_tasks.erl +++ b/apps/dreki_web/src/ui/dreki_web_ui_tasks.erl diff --git a/apps/dreki_web/templates/crash.dtl b/apps/dreki_web/templates/crash.dtl new file mode 100644 index 0000000..b0a291c --- /dev/null +++ b/apps/dreki_web/templates/crash.dtl @@ -0,0 +1,52 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>{% if page_title %}{{ page_title }} - {% endif %}{{site_title}}</title> + <link rel="stylesheet" href="/static/app.css"> + <script src="/static/app.js" defer></script> + <link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png"> +</head> +<body class="dark:bg-[#2E0E02] dark:text-orange-100 bg-orange-100 text-orange-900"> +<header class="bg-orange-600 dark:bg-red-900"> +<div class="container mx-auto sm:px-6 lg:px-8"> + + <nav class="" aria-label="Top"> + <div class="w-full py-6 flex items-center justify-between border-b border-primary-500 lg:border-none"> + <div class="flex items-center"> + <a href="/admin" class="text-primary-100 hover:text-white text-lg"> + <span class="sr-only">{{site_title}}</span> +<div class="text-[2rem] inline-block">😈</span> +<!--<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> + <path stroke-linecap="round" stroke-linejoin="round" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" /> +</svg>--> + </a> + </nav> +</div> +</header> + +<div class="container mx-auto sm:px-6 lg:px-8 sm:py-4 lg:py-6"> +<div class="content"> + <h1 class="text-7xl text-red-600 dark:text-red-100 font-bold font-mono">😤<br/>{{ class }}</h1> + <h2 class="text-4xl text-orange-800 dark:text-orange-300 font-bold font-mono mt-10 mb-10">{{ reason }}</h2> + {% for line in stacktrace %} + <h3 class="text-lg text-stone-400 dark:text-stone:500 -mb-2 font-mono">{{ line }}</h3> + {% endfor %} +</div> +</div> + +<footer class=""> + <div class="container mx-auto sm:px-6 lg:px-8 sm:py-4 lg:py-6"> + <p class="mt-8 text-xs text-gray-400 font-mono"> + node: {{ dreki_node }} + </p> + <p class="mt-2 text-xs text-gray-400 font-mono"> + trace: <a href="https://grafana.adm.random.sh/explore?orgId=1&left=%5B%22now-1h%22,%22now%22,%22Tempo%20(Stairway)%22,%7B%22refId%22:%22A%22,%22queryType%22:%22traceId%22,%22query%22:%22{{trace_id}}%22%7D%5D" target="_blank">{{ trace_id }}</a> + </p> + </footer> +</div> +</body> +</html> diff --git a/apps/dreki_web/templates/layout.dtl b/apps/dreki_web/templates/layout.dtl index b27fe30..a326305 100644 --- a/apps/dreki_web/templates/layout.dtl +++ b/apps/dreki_web/templates/layout.dtl @@ -73,10 +73,11 @@ <footer class=""> <div class="container mx-auto sm:px-6 lg:px-8 sm:py-4 lg:py-6"> <p class="mt-8 text-center text-sm text-gray-400"> - dreki $nodename + dreki {{dreki_node}} </p> - <p class="mt-2 text-center text-xs text-gray-300"> - identity:{{identity_id}} + <p class="mt-2 text-center text-xs text-gray-400"> + identity: {{identity_id}}<br /> + trace: <a href="https://grafana.adm.random.sh/explore?orgId=1&left=%5B%22now-1h%22,%22now%22,%22Tempo%20(Stairway)%22,%7B%22refId%22:%22A%22,%22queryType%22:%22traceId%22,%22query%22:%22{{trace_id}}%22%7D%5D" target="_blank">{{trace_id}}> </p> </footer> </div> diff --git a/apps/dreki_web/templates/store_list.dtl b/apps/dreki_web/templates/store_list.dtl index 65dc5af..81209a6 100644 --- a/apps/dreki_web/templates/store_list.dtl +++ b/apps/dreki_web/templates/store_list.dtl @@ -1,9 +1,11 @@ <h1 class="text-lg">{{ location }} :: {{ namespace }}:{{ directory }}</h1> <ul> -{% for res in result %} -<li><a href="{{ res.href }}">{{ res.id }}</a></li> +{% for res in results %} +<li><a href="{{ res.href }}" class="text-primary-700 dark:text-primary-100">{{ res.id }}</a> {{ res.params.title }}</li> {% endfor %} </ul> -<a href="{{ new }}">Create</a> +{% for action in actions %} +<a href="{{ action.href }}">{{ action.title }}</a> +{% endfor %} diff --git a/apps/dreki_web/templates/store_new.dtl b/apps/dreki_web/templates/store_new.dtl index 5a85854..0ae8d59 100644 --- a/apps/dreki_web/templates/store_new.dtl +++ b/apps/dreki_web/templates/store_new.dtl @@ -2,14 +2,18 @@ <div class="text-base"> {{ location }} / {{ namespace }} / {{ directory }} </div> - New + {% if result %} + {{ result.id }}: {{ title }} + {% else %} + New + {% endif %} </h1> -<form action="{{ target }}" method="{{ method }}"> +<form action="{{ target }}" method="{{ method }}" class="space-y-8 divide-y divide-gray-200"> {{ form | safe }} <div class="pt-5"> - <div class="flex justify-end"> + <div class="flex justify-start"> <button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">Save</button> </div> </div> diff --git a/apps/dreki_web/templates/store_show.dtl b/apps/dreki_web/templates/store_show.dtl index e944bee..3b139bb 100644 --- a/apps/dreki_web/templates/store_show.dtl +++ b/apps/dreki_web/templates/store_show.dtl @@ -5,4 +5,12 @@ {{ result.id }} </h1> +<ul> +{% for res in result %} +<li>{{res}}</li> +{% endfor %} +</ul> +{% for action in actions %} +<a href="{{ action.href }}">{{ action.title }}</a> +{% endfor %} diff --git a/config/mgmt2/sys.config b/config/mgmt2/sys.config new file mode 100644 index 0000000..cb272d4 --- /dev/null +++ b/config/mgmt2/sys.config @@ -0,0 +1,114 @@ +[ +{kernel, + [ + {logger_level, debug}, + {logger, [ + {handler, default, logger_std_h, + #{level => debug, + formatter => {logger_colorful_formatter, + #{ + colors => #{debug => blue, notice => {bright, green}, warning => yellow, error => red, + critical => {bright, red}, alert => {bright, magenta}, emergency => {bg, red}}, + template => [ + time, " ", + "level=",level," ", + {trace_id, ["trace_id=", trace_id, " "], []}, + {domain, ["domain=", domain, " "], []}, + {mfa, ["mfa=", mfa, " "], []}, + {pid, ["pid=", pid, " "], []}, + msg, " ", + %%{file, ["file=", file, ":", line, " "], []}, + "\n" + ] + }}, + filters => [ + {opentelemetry_logger_metadata, {fun opentelemetry_logger_metadata:filter/2, []}} + ] + }} + ]} + ]}, + + +{opentelemetry, [ + {span_processor, batch}, + {exporter, otlp}, + {text_map_propagators, [baggage, trace_context]} +]}, + +{opentelemetry_exporter, [ + {otlp_protocol, grpc}, + {otlp_endpoint, "http://tempo.stairway.internal.random.sh:4317"}, + {otlp_headers, []} +]}, + + + +{plum_db, [ + {aae_enabled, true}, + {store_open_retries_delay, 2000}, + {store_open_retry_Limit, 30}, + {data_exchange_timeout, 60000}, + {hashtree_timer, 10000}, + {data_dir, "data2/plumdb"}, + {partitions, 8}, + {prefixes, [ + {state, ram}, + {world, ram_disk}, + {names, ram_disk}, + {regions, ram_disk}, + {nodes, ram_disk}, + {paths, ram_disk}, + {tasks, ram_disk}, + {objects, ram_disk}, + {stores, ram_disk}, + {'idx:roles', ram_disk}, {'idx:tags', ram_disk} + ]} +]}, +{partisan, [ + {peer_ip, {0,0,0,0}}, + {peer_port, 18087}, % port for inter-node communication + {parallelism, 4}, % number of tcp connections + {pid_encoding, false}, + {ref_encoding, false}, + {exchange_tick_period, 60000}, + {lazy_tick_period, 1000}, + {partisan_peer_service_manager, + partisan_pluggable_peer_service_manager} +]}, +{plumtree, [ + {broadcast_exchange_timer, 60000} % Perform AAE exchange every 1 min. +]}, + {dreki, [ + {root_domain, <<"random.sh">>}, + {internal_domain, <<"inf.random.sh">>}, + {domain, <<"mgmt2.stairway.dc2.scw.fr.eu.inf.random.sh">>}, + {local_tasks_stores, [ + {<<"local">>, dreki_dets_tasks, #{}, #{}} + ]}, + {autojoin, [<<"mgmt.stairway.dc2.scw.fr.eu.inf.random.sh">>]}, + {local_names_store, [ + %% Store for *.DOMAIN + {local, dreki_dets_names, []}, + %% Store for XXX.DOMAIN + {<<"service">>, dreki_dets_names, []} + ]}, + {local_sequences_store, {dreki_dets_sequences, []}} + ]}, + {ory, [ + {kratos_url, <<"https://kratos.sso.internal.random.sh">>}, + {hydra_url, <<"https://hydra.sso.internal.random.sh">>}, + {keto_url, <<"https://keto.sso.internal.random.sh">>}, + {hackney_ssl_opts, [ + {verify, verify_peer}, + {versions, ['tlsv1.2', 'tlsv1.3']}, + {cacertfile, "/usr/local/etc/ssl/certs/ca.internal.random.sh.crt"}, + {crl_check, false}, + {crl_cache, {ssl_crl_cache, {internal, [{http, 5000}]}}} + ]} + ]}, + {dreki_web, [ + {transport, [ + {port, 5001} + ]} + ]} +]. diff --git a/config/mgmt2/vm.args b/config/mgmt2/vm.args new file mode 100644 index 0000000..ea807ca --- /dev/null +++ b/config/mgmt2/vm.args @@ -0,0 +1,9 @@ +-name dreki2@mgmt2.stairway.dc2.scw.fr.eu.inf.random.sh + +-setcookie dreki_cookie + ++K true ++A30 + +## Enable multi_time_warp ++C multi_time_warp diff --git a/config/sys.config b/config/sys.config index be53a1f..5037a42 100644 --- a/config/sys.config +++ b/config/sys.config @@ -1,20 +1,32 @@ [ -{kernel, [ - {logger_level, info}, - {logger, [ - {handler, default, logger_std_h, #{ - formatter => - {logger_colorful_formatter, #{ - colors => #{debug => blue, notice => {bright, green}, warning => yellow, error => red, - critical => {bright, red}, alert => {bright, magenta}, emergency => {bg, red}}, - template => [time," [",level,"] ", file,":",line," ",msg,"\n"] - }} - }} -%%#{ formatter => -%% {logger_colorful_formatter, #{}} %%#{ template => [time," [",level,"] ", file,":",line," ",msg,"\n\n"] }} -%% }} - ]} -]}, +{kernel, + [ + {logger_level, debug}, + {logger, [ + {handler, default, logger_std_h, + #{level => info, + formatter => {logger_colorful_formatter, + #{ + colors => #{debug => blue, notice => {bright, green}, warning => yellow, error => red, + critical => {bright, red}, alert => {bright, magenta}, emergency => {bg, red}}, + template => [ + time, " ", + "level=",level," ", + {trace_id, ["trace_id=", trace_id, " "], []}, + {domain, ["domain=", domain, " "], []}, + {mfa, ["mfa=", mfa, " "], []}, + {pid, ["pid=", pid, " "], []}, + msg, " ", + %%{file, ["file=", file, ":", line, " "], []}, + "\n" + ] + }}, + filters => [ + {opentelemetry_logger_metadata, {fun opentelemetry_logger_metadata:filter/2, []}} + ] + }} + ]} + ]}, %%{lager, [ %% {error_logger_redirect, false}, @@ -23,18 +35,14 @@ {opentelemetry, [ {span_processor, batch}, - {exporter, {otel_exporter_stdout, []}}, + {exporter, otlp}, {text_map_propagators, [baggage, trace_context]} ]}, {opentelemetry_exporter, [ - {oltp_protocol, grpc}, - {otlp_endpoint, "https://tempo-us-central1.grafana.net:443"}, - {oltp_headers, [ - %% 38972:eyJrIjoiMzg1ZjEwZTg3YjU0ZDY4ZGQzZTg3MzllNzU4NGZlZjI1NmQ5YWRhMCIsIm4iOiJkcmVraSBkZXYiLCJpZCI6NDg1NjI5fQ== - {"authorization", - "Basic Mzg5NzI6ZXlKcklqb2lNemcxWmpFd1pUZzNZalUwWkRZNFpHUXpaVGczTXpsbE56VTROR1psWmpJMU5tUTVZV1JoTUNJc0ltNGlPaUprY21WcmFTQmtaWFlpTENKcFpDSTZORGcxTmpJNWZRPT0="} - ]} + {otlp_protocol, grpc}, + {otlp_endpoint, "http://tempo.stairway.internal.random.sh:4317"}, + {otlp_headers, []} ]}, {plum_db, [ diff --git a/rebar.config b/rebar.config index 05d8e4c..99d2e26 100644 --- a/rebar.config +++ b/rebar.config @@ -5,6 +5,15 @@ {opentelemetry_api, "~> 1.0"}, {opentelemetry, "~> 1.0"}, {opentelemetry_exporter, "1.0.2"}, + {opentelemetry_process_propagator, "0.1.1"}, + {opentelemetry_logger_metadata, "0.1.0"}, + {opentelemetry_telemetry, "~> 1.0.0-beta.7"}, + {telemetry, "~> 1.0"}, + {prometheus, "~> 4.8.2"}, + + {circuit_breaker, {git, "https://github.com/klarna/circuit_breaker", {branch, "master"}}}, + {sbroker, "1.0.0"}, + {genlib, {git, "https://github.com/rbkmoney/genlib", {branch, "master"}}}, {uuid, "2.0.4", {pkg, uuid_erl}}, {ory, {git, "https://git.random.sh/erlang-ory.git", {branch, "main"}}}, {mnesia_rocksdb, {git, "https://github.com/aeternity/mnesia_rocksdb", {branch, "master"}}}, @@ -30,7 +39,7 @@ rebar3_lint ]}. -%%{elvis_output_format, plain | colors | parsable}. +%%{elvis_output_format, plain | colors | parsable}. {elvis_output_format, parsable}. {elvis, [ #{ dirs => ["apps/*/src/**", "src/**"], @@ -1,5 +1,6 @@ {"1.2.0", -[{<<"acceptor_pool">>,{pkg,<<"acceptor_pool">>,<<"1.0.0">>},1}, +[{<<"accept">>,{pkg,<<"accept">>,<<"0.3.5">>},2}, + {<<"acceptor_pool">>,{pkg,<<"acceptor_pool">>,<<"1.0.0">>},1}, {<<"app_config">>, {git,"https://gitlab.com/leapsight/app_config.git", {ref,"e6a4dc99c0c9f17a6d4d865e40aefc409986f849"}}, @@ -14,6 +15,10 @@ {<<"bear">>,{pkg,<<"bear">>,<<"0.8.7">>},2}, {<<"certifi">>,{pkg,<<"certifi">>,<<"2.6.1">>},2}, {<<"chatterbox">>,{pkg,<<"ts_chatterbox">>,<<"0.11.0">>},2}, + {<<"circuit_breaker">>, + {git,"https://github.com/klarna/circuit_breaker", + {ref,"ca94b561605e6319fabd45a2120c0cfd3edf6820"}}, + 0}, {<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.9.0">>},0}, {<<"cowboy_telemetry">>,{pkg,<<"cowboy_telemetry">>,<<"0.4.0">>},0}, {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.11.0">>},1}, @@ -37,6 +42,10 @@ {ref,"dd6fb59a82ad0b920baee97271826b1ae255d1b4"}}, 0}, {<<"erlsom">>,{pkg,<<"erlsom">>,<<"1.5.0">>},2}, + {<<"erltrace">>, + {git,"https://gitlab.com/Project-FiFo/FiFo/erltrace.git", + {ref,"66d0c3c42284fb692525dd65f89cf1446b8787b0"}}, + 0}, {<<"erlydtl">>,{pkg,<<"erlydtl">>,<<"0.14.0">>},0}, {<<"fast_yaml">>, {git,"https://github.com/processone/fast_yaml", @@ -44,12 +53,17 @@ 0}, {<<"folsom">>,{pkg,<<"folsom">>,<<"0.8.8">>},1}, {<<"gen_batch_server">>,{pkg,<<"gen_batch_server">>,<<"0.8.7">>},1}, + {<<"genlib">>, + {git,"https://github.com/rbkmoney/genlib", + {ref,"82c5ff3866e3019eb347c7f1d8f1f847bed28c10"}}, + 0}, {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1}, {<<"gproc">>,{pkg,<<"gproc">>,<<"0.9.0">>},1}, {<<"grpcbox">>,{pkg,<<"grpcbox">>,<<"0.14.0">>},1}, {<<"hackney">>,{pkg,<<"hackney">>,<<"1.17.4">>},1}, {<<"hpack">>,{pkg,<<"hpack_erl">>,<<"0.2.3">>},3}, {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},2}, + {<<"inet_cidr">>,{pkg,<<"erl_cidr">>,<<"1.2.0">>},0}, {<<"iso8601">>,{pkg,<<"iso8601">>,<<"1.3.1">>},1}, {<<"jesse">>, {git,"https://github.com/for-GET/jesse", @@ -97,6 +111,12 @@ {<<"opentelemetry_exporter">>, {pkg,<<"opentelemetry_exporter">>,<<"1.0.2">>}, 0}, + {<<"opentelemetry_logger_metadata">>, + {pkg,<<"opentelemetry_logger_metadata">>,<<"0.1.0">>}, + 0}, + {<<"opentelemetry_process_propagator">>, + {pkg,<<"opentelemetry_process_propagator">>,<<"0.1.1">>}, + 0}, {<<"opentelemetry_telemetry">>, {pkg,<<"opentelemetry_telemetry">>,<<"1.0.0-beta.7">>}, 1}, @@ -114,6 +134,10 @@ {git,"https://gitlab.com/leapsight/plum_db", {ref,"49ab98faed623e26a2293eac0b73bed34b02e47a"}}, 0}, + {<<"prometheus">>,{pkg,<<"prometheus">>,<<"4.8.2">>},0}, + {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.1.8">>},0}, + {<<"prometheus_httpd">>,{pkg,<<"prometheus_httpd">>,<<"2.1.11">>},1}, + {<<"quantile_estimator">>,{pkg,<<"quantile_estimator">>,<<"0.2.1">>},1}, {<<"quickrand">>,{pkg,<<"quickrand">>,<<"2.0.4">>},1}, {<<"ra">>,{pkg,<<"ra">>,<<"2.0.6">>},0}, {<<"ranch">>,{pkg,<<"ranch">>,<<"1.8.0">>},1}, @@ -124,6 +148,7 @@ 1}, {<<"rfc3339">>,{pkg,<<"rfc3339">>,<<"0.2.2">>},1}, {<<"rocksdb">>,{pkg,<<"rocksdb">>,<<"1.7.0">>},1}, + {<<"sbroker">>,{pkg,<<"sbroker">>,<<"1.0.0">>},0}, {<<"sext">>,{pkg,<<"sext">>,<<"1.8.0">>},1}, {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},2}, {<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.1.0">>},1}, @@ -142,6 +167,7 @@ {<<"yamerl">>,{pkg,<<"yamerl">>,<<"0.10.0">>},0}]}. [ {pkg_hash,[ + {<<"accept">>, <<"B33B127ABCA7CC948BBE6CAA4C263369ABF1347CFA9D8E699C6D214660F10CD1">>}, {<<"acceptor_pool">>, <<"43C20D2ACAE35F0C2BCD64F9D2BDE267E459F0F3FD23DAB26485BF518C281B21">>}, {<<"aten">>, <<"F88EB38CEADC0710B84B5D78F0357D766733D80902D50AC69B70CDF834992DAA">>}, {<<"base32">>, <<"044F6DC95709727CA2176F3E97A41DDAA76B5BC690D3536908618C0CB32616A2">>}, @@ -165,6 +191,7 @@ {<<"hackney">>, <<"99DA4674592504D3FB0CFEF0DB84C3BA02B4508BAE2DFF8C0108BAA0D6E0977C">>}, {<<"hpack">>, <<"17670F83FF984AE6CD74B1C456EDDE906D27FF013740EE4D9EFAA4F1BF999633">>}, {<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>}, + {<<"inet_cidr">>, <<"9205FFB290C0DE8D2B82147976602FBF5BFA6D594834E60556AFAF3B82856B95">>}, {<<"iso8601">>, <<"D1CEE73F56D71C35590C6B2DB2074873BF410BABAAB768F6EA566366D8CA4810">>}, {<<"jsone">>, <<"7EA1098FE004C4127320FE0E3CF6A951B01F82039FEAA56C322DC7E34DD59762">>}, {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}, @@ -178,15 +205,22 @@ {<<"opentelemetry_api">>, <<"91353EE40583B1D4F07D7B13ED62642ABFEC6AAA0D8A2114F07EDAFB2DF781C5">>}, {<<"opentelemetry_cowboy">>, <<"CE16BE94932DFCF9A036DE0B516413A5180AA6A4F31AD4073DD86EABE73F8837">>}, {<<"opentelemetry_exporter">>, <<"19A102D1F04776399A915BE27121852468318C27146E553FAF28008E3E474972">>}, + {<<"opentelemetry_logger_metadata">>, <<"0D1B7A4669521D75B142C9C9585771ED0707CCD515312CF06135C5BFC5C60AB9">>}, + {<<"opentelemetry_process_propagator">>, <<"81EC6825971903486EE73BE23230D06764DF39EE11011E520F601AA2BB21C893">>}, {<<"opentelemetry_telemetry">>, <<"BA1DF62515AED63F99A80DDF17E7A3873D1F686F23598EDEBF1633942772856E">>}, {<<"p1_utils">>, <<"7F94466ADA69BD982EA7BB80FBCA18E7053E7D0B82C9D9E37621FA508587069B">>}, {<<"parse_trans">>, <<"16328AB840CC09919BD10DAB29E431DA3AF9E9E7E7E6F0089DD5A2D2820011D8">>}, + {<<"prometheus">>, <<"B88F24279DD7A1F512CB090595FF6C88B50AAD0A6B394A4C4983725723DCD834">>}, + {<<"prometheus_cowboy">>, <<"CFCE0BC7B668C5096639084FCD873826E6220EA714BF60A716F5BD080EF2A99C">>}, + {<<"prometheus_httpd">>, <<"F616ED9B85B536B195D94104063025A91F904A4CFC20255363F49A197D96C896">>}, + {<<"quantile_estimator">>, <<"EF50A361F11B5F26B5F16D0696E46A9E4661756492C981F7B2229EF42FF1CD15">>}, {<<"quickrand">>, <<"168CA3A8466A26912B8C3A1D6AA58975E1BB49E5C7AFB4998B80F6B90F910490">>}, {<<"ra">>, <<"C1AD68DE00B5DD3B32E6A30E1359AC676C3CAE47EECBA951789B470CBD4E1087">>}, {<<"ranch">>, <<"8C7A100A139FD57F17327B6413E4167AC559FBC04CA7448E9BE9057311597A1D">>}, {<<"recon">>, <<"CBA53FA8DB83AD968C9A652E09C3ED7DDCC4DA434F27C3EAA9CA47FFB2B1FF03">>}, {<<"rfc3339">>, <<"1552DF616ACA368D982E9F085A0E933B6688A3F4938A671798978EC2C0C58730">>}, {<<"rocksdb">>, <<"5D23319998A7FCE5FFD5D7824116C905CABA7F91BAF8EDDABD0180F1BB272CEF">>}, + {<<"sbroker">>, <<"28FF1B5E58887C5098539F236307B36FE1D3EDAA2ACFF9D6A3D17C2DCAFEBBD0">>}, {<<"sext">>, <<"90A95B889F5C781B70BBCF44278B763148E313C376B60D87CE664CB1C1DD29B5">>}, {<<"ssl_verify_fun">>, <<"CF344F5692C82D2CD7554F5EC8FD961548D4FD09E7D22F5B62482E5AEAEBD4B0">>}, {<<"telemetry">>, <<"A589817034A27EAB11144AD24D5C0F9FAB1F58173274B1E9BAE7074AF9CBEE51">>}, @@ -198,6 +232,7 @@ {<<"uuid">>, <<"77C3E3EE1E1701A2856CE945846D7CEB71931C60633A305D0B0FEAE03B2B3B5C">>}, {<<"yamerl">>, <<"4FF81FEE2F1F6A46F1700C0D880B24D193DDB74BD14EF42CB0BCF46E81EF2F8E">>}]}, {pkg_hash_ext,[ + {<<"accept">>, <<"11B18C220BCC2EAB63B5470C038EF10EB6783BCB1FCDB11AA4137DEFA5AC1BB8">>}, {<<"acceptor_pool">>, <<"0CBCD83FDC8B9AD2EEE2067EF8B91A14858A5883CB7CD800E6FCD5803E158788">>}, {<<"aten">>, <<"8B623C8BE27B59A911D16AB0AF41777B504C147BC0D60A29015FAB58321C04B0">>}, {<<"base32">>, <<"10A73951D857D8CB1ECEEA8EB96C6941F6A76E105947AD09C2B73977DEE07638">>}, @@ -221,6 +256,7 @@ {<<"hackney">>, <<"DE16FF4996556C8548D512F4DBE22DD58A587BF3332E7FD362430A7EF3986B16">>}, {<<"hpack">>, <<"06F580167C4B8B8A6429040DF36CC93BBA6D571FAEAEC1B28816523379CBB23A">>}, {<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>}, + {<<"inet_cidr">>, <<"3505F5DFAC7D862806C7051A3DD475363A45BCCF39CA1FAEE8EDA6A6B33CF335">>}, {<<"iso8601">>, <<"A8B00594F4309A41D17BA4AEAB2B94DFE1F4BE99F263BC1F46DAC9002CE99A29">>}, {<<"jsone">>, <<"A6C1DF6081DF742068D2ED747A4FE8A7740C56421B53E02BC9D4907DD3502922">>}, {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}, @@ -234,15 +270,22 @@ {<<"opentelemetry_api">>, <<"2A8247F85C44216B883900067478D59955D11E58E5CFCA7C884CD4F203ACE3AC">>}, {<<"opentelemetry_cowboy">>, <<"455263DD177ACB8724391A5F2AF809E99F5D4A6DC71684E34A462F4D4AD02BD0">>}, {<<"opentelemetry_exporter">>, <<"43DE904DD7F482009BF1A40D591AB5ED25F603F1072A04A162E87F7F8C08DBB5">>}, + {<<"opentelemetry_logger_metadata">>, <<"772976D3C59651CF9C4600EDC238BB9CADF7F5EDAED1A1C5C59BF3E773DFE9FC">>}, + {<<"opentelemetry_process_propagator">>, <<"0572F26066BBB0457E22E169F966C0140A8F95237716C9C6BA4458D6DBAA724B">>}, {<<"opentelemetry_telemetry">>, <<"480F4FA1E992D597F931E7BC9E68478E8D904AD84489D2C5CA6EB6D48BBD7801">>}, {<<"p1_utils">>, <<"47F21618694EEEE5006AF1C88731AD86B757161E7823C29B6F73921B571C8502">>}, {<<"parse_trans">>, <<"07CD9577885F56362D414E8C4C4E6BDF10D43A8767ABB92D24CBE8B24C54888B">>}, + {<<"prometheus">>, <<"C3ABD6521E52CEC4F0D8ECA603CF214DFC84D8A27AA85946639F1424B8554D98">>}, + {<<"prometheus_cowboy">>, <<"BA286BECA9302618418892D37BCD5DC669A6CC001F4EB6D6AF85FF81F3F4F34C">>}, + {<<"prometheus_httpd">>, <<"0BBE831452CFDF9588538EB2F570B26F30C348ADAE5E95A7D87F35A5910BCF92">>}, + {<<"quantile_estimator">>, <<"282A8A323CA2A845C9E6F787D166348F776C1D4A41EDE63046D72D422E3DA946">>}, {<<"quickrand">>, <<"4CB18E9304CF28E054E8DC6E151D1AC7F174E6FE31D5C1A07F71279B92A90800">>}, {<<"ra">>, <<"75C664334D4F294327DD8AAD06385294CC5CCE4763FBEF13EBF5075E930CEDF6">>}, {<<"ranch">>, <<"49FBCFD3682FAB1F5D109351B61257676DA1A2FDBE295904176D5E521A2DDFE5">>}, {<<"recon">>, <<"2C7523C8DEE91DFF41F6B3D63CBA2BD49EB6D2FE5BF1EEC0DF7F87EB5E230E1C">>}, {<<"rfc3339">>, <<"986D7F9BAC6891AA4D5051690058DE4E623245620BBEADA7F239F85C4DF8F23C">>}, {<<"rocksdb">>, <<"A4BDC5DD80ED137161549713062131E8240523787EBE7B51DF61CFB48B1786CE">>}, + {<<"sbroker">>, <<"BA952BFA35B374E1E5D84BC5F5EFE8360C6F99DC93B3118F714A9A2DFF6C9E19">>}, {<<"sext">>, <<"BC6016CB8690BAF677EACACFE6E7CADFEC8DC7E286CBBED762F6CD55B0678E73">>}, {<<"ssl_verify_fun">>, <<"BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680">>}, {<<"telemetry">>, <<"B727B2A1F75614774CFF2D7565B64D0DFA5BD52BA517F16543E6FC7EFCC0DF48">>}, |