Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/hellgate/src/hellgate.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
limiter_proto,
opentelemetry_api,
opentelemetry_exporter,
opentelemetry
opentelemetry,
opentelemetry_experimental,
opentelemetry_api_experimental
]},
{env, []},
{modules, []},
Expand Down
70 changes: 68 additions & 2 deletions apps/hellgate/src/hellgate.erl
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,81 @@ get_prometheus_route() ->

-spec start(normal, any()) -> {ok, pid()} | {error, any()}.
start(_StartType, _StartArgs) ->
ok = setup_metrics(),
supervisor:start_link(?MODULE, []).
case ensure_otel_log_handler() of
ok ->
ok = setup_metrics(),
supervisor:start_link(?MODULE, []);
{error, Reason} ->
logger:error("Failed to add otel_logs handler: ~p", [Reason]),
{error, Reason}
end.

-spec stop(any()) -> ok.
stop(_State) ->
ok = flush_otel_logs(),
ok.

%%

setup_metrics() ->
ok = woody_ranch_prometheus_collector:setup(),
ok = woody_hackney_prometheus_collector:setup().

ensure_otel_log_handler() ->
case logger:get_handler_config(otel_logs) of
{ok, _} ->
ok;
_ ->
MaxQueue = application:get_env(hellgate, otel_log_max_queue_size, 2048),
DelayMs = application:get_env(hellgate, otel_log_scheduled_delay_ms, 1000),
TimeoutMs = application:get_env(hellgate, otel_log_exporting_timeout_ms, 300000),
LogLevel = application:get_env(hellgate, otel_log_level, info),
HandlerConfig = #{
report_cb => fun hg_otel_log_filter:format_otp_report_utf8/1,
exporter =>
{otel_exporter_logs_otlp, #{
protocol => http_protobuf,
ssl_options => []
}},
max_queue_size => MaxQueue,
scheduled_delay_ms => DelayMs,
exporting_timeout_ms => TimeoutMs
},
LoggerHandlerConfig = #{
level => LogLevel,
filter_default => log,
filters => [{hg_otel_trace_id_bytes, {fun hg_otel_log_filter:filter/2, undefined}}],
config => HandlerConfig
},
case logger:add_handler(otel_logs, hg_otel_log_handler, LoggerHandlerConfig) of
ok ->
ok;
{error, {already_exist, _}} ->
ok;
{error, Reason} ->
{error, {otel_log_handler_failed, Reason}}
end
end.

%% @doc Ждём отправки буферизованных логов перед остановкой.
%% otel_log_handler батчит логи и отправляет по таймеру (scheduled_delay_ms).
%% Явного API для flush у otel_log_handler нет, поэтому ждём один полный цикл
%% батчинга + запас на сетевую отправку (export overhead).
-define(FLUSH_EXPORT_OVERHEAD_MS, 700).
-define(FLUSH_MAX_WAIT_MS, 5000).

flush_otel_logs() ->
case logger:get_handler_config(otel_logs) of
{ok, HandlerCfg} ->
Config = maps:get(config, HandlerCfg, #{}),
DelayMs = maps:get(
scheduled_delay_ms,
Config,
maps:get(scheduled_delay_ms, HandlerCfg, 1000)
),
_ = logger:info("otel_log_handler_flush"),
timer:sleep(erlang:min(?FLUSH_MAX_WAIT_MS, DelayMs + ?FLUSH_EXPORT_OVERHEAD_MS)),
ok;
_ ->
ok
end.
65 changes: 65 additions & 0 deletions apps/hellgate/src/hg_otel_log_filter.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
%%% @doc
%%% Logger filter для otel_logs handler: конвертирует otel_trace_id и otel_span_id
%%% из hex-формата (32/16 символов) в raw bytes (16/8 байт), как требует OTLP LogRecord.
%%% opentelemetry hex_span_ctx возвращает hex, collector ожидает bytes.
%%% @end
-module(hg_otel_log_filter).

-export([filter/2]).
-export([format_otp_report_utf8/1]).

-spec filter(logger:log_event(), term()) -> logger:filter_return().
filter(#{meta := Meta} = LogEvent, _FilterConfig) ->
case convert_otel_ids(Meta) of
Meta ->
LogEvent;
Meta1 ->
LogEvent#{meta => Meta1}
end.

%% Конвертируем hex -> raw bytes только если формат hex (32/16 символов).
%% OTLP LogRecord: trace_id=16 bytes, span_id=8 bytes.
convert_otel_ids(#{otel_trace_id := TraceIdHex, otel_span_id := SpanIdHex} = Meta) ->
case {hex_to_trace_id_bytes(TraceIdHex), hex_to_span_id_bytes(SpanIdHex)} of
{TraceIdBytes, SpanIdBytes} when TraceIdBytes =/= undefined, SpanIdBytes =/= undefined ->
Meta#{otel_trace_id => TraceIdBytes, otel_span_id => SpanIdBytes};
_ ->
%% Некорректный формат — убираем, чтобы otel_otlp_logs не отправил в OTLP
maps:without([otel_trace_id, otel_span_id, otel_trace_flags], Meta)
end;
convert_otel_ids(Meta) ->
Meta.

%% logger:format_otp_report/1 возвращает chardata (часто list()),
%% из-за чего downstream JSON может сериализовать body как массив байт.
%% Явно приводим к UTF-8 binary(), чтобы body в OTel/Loki был строкой.
-spec format_otp_report_utf8(logger:report()) -> {unicode:chardata(), list()}.
format_otp_report_utf8(Report) ->
Bin =
try logger:format_otp_report(Report) of
{Format, Args} ->
unicode:characters_to_binary(io_lib:format(Format, Args))
catch
_:_ ->
%% Не даём report_cb падать: fallback в печатное представление отчёта.
unicode:characters_to_binary(io_lib:format("~tp", [Report]))
end,
{"~ts", [Bin]}.

hex_to_trace_id_bytes(Hex) when is_binary(Hex), byte_size(Hex) =:= 32 ->
try
<<(binary_to_integer(Hex, 16)):128>>
catch
_:_ -> undefined
end;
hex_to_trace_id_bytes(_) ->
undefined.

hex_to_span_id_bytes(Hex) when is_binary(Hex), byte_size(Hex) =:= 16 ->
try
<<(binary_to_integer(Hex, 16)):64>>
catch
_:_ -> undefined
end;
hex_to_span_id_bytes(_) ->
undefined.
48 changes: 48 additions & 0 deletions apps/hellgate/src/hg_otel_log_handler.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-module(hg_otel_log_handler).

-export([log/2]).
-export([adding_handler/1]).
-export([removing_handler/1]).
-export([changing_config/3]).
-export([filter_config/1]).

-spec log(logger:log_event(), map()) -> ok.
log(LogEvent, Config) ->
otel_log_handler:log(LogEvent, Config).

-spec adding_handler(map()) -> {ok, map()} | {error, term()}.
adding_handler(Config) ->
otel_log_handler:adding_handler(merge_module_config(Config)).

-spec removing_handler(map()) -> ok.
removing_handler(Config) ->
otel_log_handler:removing_handler(Config).

-spec changing_config(set | update, map(), map()) -> {ok, map()} | {error, term()}.
changing_config(SetOrUpdate, OldConfig, NewConfig) ->
otel_log_handler:changing_config(
SetOrUpdate,
merge_module_config(OldConfig),
merge_module_config(NewConfig)
).

-spec filter_config(map()) -> map().
filter_config(Config) ->
otel_log_handler:filter_config(Config).

%% Переносим ключи из вложенного config в корневой map для otel_log_handler.
%% Только ключи, которые ожидает otel_log_handler — чтобы случайно не перезаписать
%% верхнеуровневые настройки logger (level, filters, filter_default и т.д.).
-define(OTEL_LOG_HANDLER_KEYS, [
exporter,
report_cb,
max_queue_size,
scheduled_delay_ms,
exporting_timeout_ms
]).

merge_module_config(#{config := ModuleConfig} = Config) when is_map(ModuleConfig) ->
OtelConfig = maps:with(?OTEL_LOG_HANDLER_KEYS, ModuleConfig),
maps:merge(Config, OtelConfig);
merge_module_config(Config) ->
Config.
Loading
Loading