%%%------------------------------------------------------------------- %%% @doc Distributed tracing helpers for barrel_docdb %%% %%% Provides OpenTelemetry-compatible tracing with semantic conventions %%% for database operations following the OpenTelemetry Database Spans %%% specification. %%% %%% Semantic conventions: %%% - db.system.name: "barrel" %%% - db.namespace: database name %%% - db.collection.name: collection/table name %%% - db.operation.name: operation type (get, put, delete, query) %%% - db.query.text: query text (sanitized) %%% - db.response.returned_rows: number of rows returned %%% %%% @end %%%------------------------------------------------------------------- -module(barrel_trace). %% Span creation with DB semantics -export([ with_db_span/3, with_db_span/4, with_http_span/3, with_http_span/4 ]). %% Attribute helpers -export([ db_attributes/1, db_attributes/2, http_attributes/2, http_attributes/3 ]). %% Context propagation for HTTP -export([ inject_headers/1, extract_headers/1, attach_context/1, with_extracted_context/2 ]). %% Recording check for expensive operations -export([ is_recording/0 ]). %% Error recording -export([ record_error/1, record_error/2 ]). %% Span access -export([ set_attribute/2, set_attributes/1, add_event/1, add_event/2 ]). %% Constants -define(DB_SYSTEM, <<"barrel">>). %%==================================================================== %% Span Creation %%==================================================================== %% @doc Execute a function within a database span. %% Creates a span with OpenTelemetry database semantic conventions. %% %% Example: %% ``` %% barrel_trace:with_db_span(put, Db, fun() -> %% do_put(Db, DocId, Doc) %% end). %% ''' -spec with_db_span(atom(), binary() | undefined, fun(() -> Result)) -> Result when Result :: term(). with_db_span(Op, Db, Fun) -> with_db_span(Op, Db, #{}, Fun). %% @doc Execute a function within a database span with extra attributes. -spec with_db_span(atom(), binary() | undefined, map(), fun(() -> Result)) -> Result when Result :: term(). with_db_span(Op, Db, ExtraAttrs, Fun) -> SpanName = span_name(Op, Db), BaseAttrs = db_attributes(Db, Op), Attrs = maps:merge(BaseAttrs, ExtraAttrs), Opts = #{ kind => internal, attributes => Attrs }, instrument_tracer:with_span(SpanName, Opts, Fun). %% @doc Execute a function within an HTTP server span. %% Creates a span for incoming HTTP requests. -spec with_http_span(binary(), binary(), fun(() -> Result)) -> Result when Result :: term(). with_http_span(Method, Path, Fun) -> with_http_span(Method, Path, #{}, Fun). %% @doc Execute a function within an HTTP server span with extra attributes. -spec with_http_span(binary(), binary(), map(), fun(() -> Result)) -> Result when Result :: term(). with_http_span(Method, Path, ExtraAttrs, Fun) -> SpanName = http_span_name(Method, Path), BaseAttrs = http_attributes(Method, Path), Attrs = maps:merge(BaseAttrs, ExtraAttrs), Opts = #{ kind => server, attributes => Attrs }, instrument_tracer:with_span(SpanName, Opts, Fun). %%==================================================================== %% Attribute Helpers %%==================================================================== %% @doc Generate database attributes for a span. -spec db_attributes(binary() | undefined) -> map(). db_attributes(Db) -> db_attributes(Db, undefined). %% @doc Generate database attributes with operation. -spec db_attributes(binary() | undefined, atom() | undefined) -> map(). db_attributes(Db, Op) -> Attrs0 = #{<<"db.system.name">> => ?DB_SYSTEM}, Attrs1 = case Db of undefined -> Attrs0; _ -> Attrs0#{<<"db.namespace">> => Db} end, case Op of undefined -> Attrs1; _ -> Attrs1#{<<"db.operation.name">> => atom_to_binary(Op, utf8)} end. %% @doc Generate HTTP attributes for a span. -spec http_attributes(binary(), binary()) -> map(). http_attributes(Method, Path) -> #{ <<"http.request.method">> => Method, <<"url.path">> => Path }. %% @doc Generate HTTP attributes with status code. -spec http_attributes(binary(), binary(), integer()) -> map(). http_attributes(Method, Path, StatusCode) -> Attrs = http_attributes(Method, Path), Attrs#{<<"http.response.status_code">> => StatusCode}. %%==================================================================== %% Context Propagation %%==================================================================== %% @doc Inject trace context into HTTP headers. %% Returns a list of {HeaderName, HeaderValue} tuples to add to outgoing requests. -spec inject_headers(list()) -> list(). inject_headers(Headers) when is_list(Headers) -> Ctx = instrument_context:current(), TraceHeaders = instrument_propagation:inject_headers(Ctx), TraceHeaders ++ Headers. %% @doc Extract trace context from HTTP headers. %% Returns the extracted context (attached to current process). %% @deprecated Use with_extracted_context/2 for proper context cleanup. -spec extract_headers(list()) -> ok. extract_headers(Headers) when is_list(Headers) -> Ctx = instrument_propagation:extract_headers(Headers), instrument_context:attach(Ctx), ok. %% @doc Attach an extracted context to the current process. %% @deprecated Use with_extracted_context/2 for proper context cleanup. -spec attach_context(map()) -> ok. attach_context(Ctx) when is_map(Ctx) -> instrument_context:attach(Ctx), ok. %% @doc Execute function with trace context extracted from HTTP headers. %% This properly scopes the context and ensures cleanup via detach. -spec with_extracted_context(list(), fun(() -> Result)) -> Result when Result :: term(). with_extracted_context(Headers, Fun) when is_list(Headers), is_function(Fun, 0) -> Ctx = instrument_propagation:extract_headers(Headers), Token = instrument_context:attach(Ctx), try Fun() after instrument_context:detach(Token) end. %% @doc Check if the current span is recording. %% Use this to guard expensive attribute computation. -spec is_recording() -> boolean(). is_recording() -> instrument_tracer:is_recording(). %%==================================================================== %% Error Recording %%==================================================================== %% @doc Record an error on the current span. -spec record_error(term()) -> ok. record_error(Error) -> record_error(Error, #{}). %% @doc Record an error with additional attributes. -spec record_error(term(), map()) -> ok. record_error(Error, Attrs) -> instrument_tracer:record_exception(Error, Attrs), ErrorMsg = format_error(Error), instrument_tracer:set_status(error, ErrorMsg), ok. %%==================================================================== %% Span Modification %%==================================================================== %% @doc Set a single attribute on the current span. -spec set_attribute(term(), term()) -> ok. set_attribute(Key, Value) -> instrument_tracer:set_attribute(Key, Value). %% @doc Set multiple attributes on the current span. -spec set_attributes(map()) -> ok. set_attributes(Attrs) when is_map(Attrs) -> instrument_tracer:set_attributes(Attrs). %% @doc Add an event to the current span. -spec add_event(binary()) -> ok. add_event(Name) -> instrument_tracer:add_event(Name). %% @doc Add an event with attributes to the current span. -spec add_event(binary(), map()) -> ok. add_event(Name, Attrs) -> instrument_tracer:add_event(Name, Attrs). %%==================================================================== %% Internal Functions %%==================================================================== %% @doc Generate span name following OpenTelemetry conventions. %% Format: {db.operation.name} {db.namespace} or {db.system.name} span_name(Op, undefined) -> OpBin = atom_to_binary(Op, utf8), <<"barrel ", OpBin/binary>>; span_name(Op, Db) -> OpBin = atom_to_binary(Op, utf8), <>. %% @doc Generate HTTP span name. %% Format: {http.method} {url.path} http_span_name(Method, Path) -> <>. %% @doc Format an error for the span status message. format_error(Error) when is_binary(Error) -> Error; format_error(Error) when is_atom(Error) -> atom_to_binary(Error, utf8); format_error({error, Reason}) -> format_error(Reason); format_error(Error) -> iolist_to_binary(io_lib:format("~p", [Error])).