├── example ├── README.md ├── 0-Scroll down for README │ └── .gitkeep ├── 1-hello-opium │ ├── README.md │ ├── dune-project │ ├── dune │ ├── .ocamlformat │ ├── demo_hello_world.opam │ ├── hello.ml │ └── Dockerfile ├── 3-polyglot-services │ ├── README.md │ ├── python │ │ ├── requirements.txt │ │ ├── Dockerfile │ │ └── app.py │ └── ocaml │ │ ├── dune-project │ │ ├── dune │ │ ├── .ocamlformat │ │ ├── demo_polyglot.opam │ │ ├── Dockerfile │ │ └── hello.ml └── 2-database-ocaml │ ├── db.mli │ ├── dune-project │ ├── dune │ ├── .ocamlformat │ ├── demo_database.opam │ ├── Dockerfile │ ├── db.ml │ └── hello.ml ├── .gitignore ├── codegen ├── system_info.mli ├── dune └── system_info.ml ├── .dockerignore ├── core ├── duration.mli ├── reporter_intf.ml ├── id.mli ├── timestamp.mli ├── duration.ml ├── request.mli ├── timestamp.ml ├── system_info.mli ├── id_intf.ml ├── error.mli ├── dune ├── request.ml ├── system_info.ml ├── context.mli ├── span.mli ├── metrics.mli ├── client_intf.ml ├── span.ml ├── transaction.mli ├── transaction.ml ├── id.ml ├── metadata.mli ├── context.ml ├── error.ml ├── metrics.ml └── metadata.ml ├── logs_reporter ├── dune ├── reporter.mli └── reporter.ml ├── lwt_reporter └── lib │ ├── dune │ ├── reporter.mli │ └── reporter.ml ├── lwt_client ├── dune ├── client.mli └── client.ml ├── rock_middleware ├── dune ├── apm.mli └── apm.ml ├── async_reporter ├── dune ├── reporter.mli └── reporter.ml ├── elastic-apm-async-client.opam.template ├── async_client ├── dune ├── client.mli └── client.ml ├── elastic-apm-async-reporter.opam.template ├── test ├── dune ├── logs_reporter_output.ml └── json_serialization.ml ├── postgres_init.sql ├── ocaml-base.Dockerfile ├── .ocamlformat ├── .github └── workflows │ └── build-client.yml ├── elastic-apm-logs-reporter.opam ├── elastic-apm-lwt-reporter.opam ├── elastic-apm-lwt-client.opam ├── elastic-apm-rock.opam ├── elastic-apm-async-reporter.opam ├── elastic-apm.opam ├── elastic-apm-async-client.opam ├── README.md ├── dune-project ├── docker-compose.yml └── LICENSE /example/README.md: -------------------------------------------------------------------------------- 1 | TODO:@pjhampton -------------------------------------------------------------------------------- /example/0-Scroll down for README/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/1-hello-opium/README.md: -------------------------------------------------------------------------------- 1 | TODO:@pjhampton -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | *.install 3 | _opam 4 | venv -------------------------------------------------------------------------------- /example/3-polyglot-services/README.md: -------------------------------------------------------------------------------- 1 | TODO:@pjhampton -------------------------------------------------------------------------------- /codegen/system_info.mli: -------------------------------------------------------------------------------- 1 | (* This file is intentionally left empty *) 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Local opam switch 2 | _opam 3 | 4 | # Dune build output 5 | _build 6 | -------------------------------------------------------------------------------- /codegen/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name system_info) 3 | (libraries dune-configurator)) 4 | -------------------------------------------------------------------------------- /core/duration.mli: -------------------------------------------------------------------------------- 1 | type t [@@deriving yojson_of] 2 | 3 | val of_span : Mtime.Span.t -> t 4 | -------------------------------------------------------------------------------- /example/3-polyglot-services/python/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.0.2 2 | elastic-apm[flask] 3 | requests 4 | -------------------------------------------------------------------------------- /core/reporter_intf.ml: -------------------------------------------------------------------------------- 1 | module type S = sig 2 | type t 3 | 4 | val push : t -> Request.t -> unit 5 | end 6 | -------------------------------------------------------------------------------- /core/id.mli: -------------------------------------------------------------------------------- 1 | module Span_id : Id_intf.S 2 | 3 | module Trace_id : Id_intf.S 4 | 5 | module Error_id : Id_intf.S 6 | -------------------------------------------------------------------------------- /core/timestamp.mli: -------------------------------------------------------------------------------- 1 | type t [@@deriving yojson_of] 2 | 3 | val now : unit -> t 4 | 5 | val of_us_since_epoch : int -> t 6 | -------------------------------------------------------------------------------- /core/duration.ml: -------------------------------------------------------------------------------- 1 | type t = Mtime.Span.t 2 | 3 | let yojson_of_t t = `Float (Mtime.Span.to_ms t) 4 | 5 | let of_span t = t 6 | -------------------------------------------------------------------------------- /logs_reporter/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name elastic_apm_logs_reporter) 3 | (public_name elastic-apm-logs-reporter) 4 | (libraries elastic-apm logs)) 5 | -------------------------------------------------------------------------------- /example/2-database-ocaml/db.mli: -------------------------------------------------------------------------------- 1 | val m : int -> Rock.Middleware.t 2 | 3 | val with_conn : Opium.Request.t -> f:(Pgx_lwt_unix.t -> 'a Lwt.t) -> 'a Lwt.t 4 | -------------------------------------------------------------------------------- /logs_reporter/reporter.mli: -------------------------------------------------------------------------------- 1 | include Elastic_apm.Reporter_intf.S 2 | 3 | val create : ?src:Logs.src -> Elastic_apm.Metadata.t -> t 4 | 5 | val src : t -> Logs.src 6 | -------------------------------------------------------------------------------- /lwt_reporter/lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name elastic_apm_lwt_reporter) 3 | (public_name elastic-apm-lwt-reporter) 4 | (libraries elastic-apm lwt.unix cohttp-lwt-unix logs)) 5 | -------------------------------------------------------------------------------- /lwt_client/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name elastic_apm_lwt_client) 3 | (public_name elastic-apm-lwt-client) 4 | (preprocess 5 | (pps lwt_ppx)) 6 | (libraries elastic-apm elastic-apm-lwt-reporter)) 7 | -------------------------------------------------------------------------------- /rock_middleware/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name elastic_apm_rock) 3 | (public_name elastic-apm-rock) 4 | (preprocess 5 | (pps lwt_ppx)) 6 | (libraries elastic-apm-lwt-client rock dune-build-info)) 7 | -------------------------------------------------------------------------------- /async_reporter/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name elastic_apm_async_reporter) 3 | (public_name elastic-apm-async-reporter) 4 | (preprocess 5 | (pps ppx_jane)) 6 | (libraries async blue_http elastic-apm)) 7 | -------------------------------------------------------------------------------- /core/request.mli: -------------------------------------------------------------------------------- 1 | type t = 2 | | Metadata of Metadata.t 3 | | Transaction of Transaction.t 4 | | Span of Span.t 5 | | Error of Error.t 6 | | Metrics of Metrics.t 7 | [@@deriving yojson_of] 8 | -------------------------------------------------------------------------------- /elastic-apm-async-client.opam.template: -------------------------------------------------------------------------------- 1 | pin-depends: [ 2 | [ 3 | "blue_http.dev" "git+https://github.com/arenadotio/blue-http.git#1140cf44d0828b7bce27cc1ec133aab471e266d0" 4 | ] 5 | ] 6 | -------------------------------------------------------------------------------- /async_client/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name elastic_apm_async_client) 3 | (public_name elastic-apm-async-client) 4 | (preprocess 5 | (pps ppx_jane)) 6 | (libraries elastic-apm elastic-apm-async-reporter)) 7 | -------------------------------------------------------------------------------- /elastic-apm-async-reporter.opam.template: -------------------------------------------------------------------------------- 1 | pin-depends: [ 2 | [ 3 | "blue_http.dev" "git+https://github.com/arenadotio/blue-http.git#1140cf44d0828b7bce27cc1ec133aab471e266d0" 4 | ] 5 | ] 6 | -------------------------------------------------------------------------------- /example/1-hello-opium/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.9) 2 | 3 | (generate_opam_files true) 4 | 5 | (package 6 | (name demo_hello_world) 7 | (depends 8 | lwt 9 | opium 10 | lwt_ppx)) 11 | -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name apm_agent_tests) 3 | (inline_tests) 4 | (preprocess 5 | (pps ppx_expect ppx_yojson_conv)) 6 | (libraries elastic-apm elastic-apm-logs-reporter fmt.tty logs.fmt yojson)) 7 | -------------------------------------------------------------------------------- /example/1-hello-opium/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name hello) 3 | (public_name demo_hello_world) 4 | (libraries base elastic-apm-rock opium logs.fmt fmt.tty) 5 | (preprocess 6 | (pps lwt_ppx ppx_yojson_conv))) 7 | -------------------------------------------------------------------------------- /example/3-polyglot-services/ocaml/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.9) 2 | 3 | (generate_opam_files true) 4 | 5 | (package 6 | (name demo_polyglot) 7 | (depends 8 | lwt 9 | opium 10 | lwt_ppx)) 11 | -------------------------------------------------------------------------------- /example/3-polyglot-services/ocaml/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name hello) 3 | (public_name dune_polyglot) 4 | (libraries base cohttp-lwt-unix opium elastic-apm-rock logs.fmt fmt.tty) 5 | (preprocess 6 | (pps lwt_ppx ppx_yojson_conv))) 7 | -------------------------------------------------------------------------------- /example/2-database-ocaml/dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.9) 2 | 3 | (generate_opam_files true) 4 | 5 | (package 6 | (name demo_database) 7 | (depends 8 | lwt 9 | opium 10 | pgx_lwt_unix 11 | pgx_value_core 12 | lwt_ppx)) 13 | -------------------------------------------------------------------------------- /core/timestamp.ml: -------------------------------------------------------------------------------- 1 | type t = int [@@deriving yojson_of] 2 | 3 | let now () : t = 4 | let t = Ptime_clock.now () in 5 | let seconds = Ptime.to_float_s t in 6 | Float.to_int (seconds *. 1000. *. 1000.) 7 | ;; 8 | 9 | let of_us_since_epoch us : t = us 10 | -------------------------------------------------------------------------------- /core/system_info.mli: -------------------------------------------------------------------------------- 1 | module Platform : sig 2 | type t = { 3 | architecture : string; 4 | hostname : string; 5 | platform : string; 6 | } 7 | [@@deriving yojson_of] 8 | 9 | val default : t lazy_t 10 | 11 | val make : architecture:string -> hostname:string -> platform:string -> t 12 | end 13 | -------------------------------------------------------------------------------- /core/id_intf.ml: -------------------------------------------------------------------------------- 1 | module type S = sig 2 | type t [@@deriving yojson_of] 3 | 4 | val equal : t -> t -> bool 5 | 6 | val create : unit -> t 7 | 8 | val create_gen : Random.State.t -> t 9 | 10 | val to_string : t -> string 11 | 12 | val to_hex : t -> string 13 | 14 | val of_hex : string -> t 15 | end 16 | -------------------------------------------------------------------------------- /example/3-polyglot-services/python/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.7-slim as build 2 | 3 | RUN apt-get update 4 | RUN apt-get install -y --no-install-recommends build-essential gcc 5 | 6 | COPY example/3-polyglot-services/python/ . 7 | RUN pip install --user -r requirements.txt 8 | 9 | ENTRYPOINT ["python", "app.py"] 10 | -------------------------------------------------------------------------------- /core/error.mli: -------------------------------------------------------------------------------- 1 | type t [@@deriving yojson_of] 2 | 3 | val make : 4 | ?random_state:Random.State.t -> 5 | ?trace_id:Id.Trace_id.t -> 6 | ?transaction_id:Id.Span_id.t -> 7 | ?parent_id:Id.Span_id.t -> 8 | exn:exn -> 9 | backtrace:Printexc.raw_backtrace -> 10 | timestamp:Timestamp.t -> 11 | unit -> 12 | t 13 | -------------------------------------------------------------------------------- /postgres_init.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 2 | 3 | DROP TABLE IF EXISTS message; 4 | 5 | CREATE TABLE message 6 | ( 7 | id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), 8 | message text, 9 | created_at timestamp DEFAULT now() 10 | ); 11 | 12 | INSERT INTO message(message) VALUES ('Hello world!'), ('Welcome to postgres'); 13 | -------------------------------------------------------------------------------- /ocaml-base.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ocaml/opam:alpine-3.14-ocaml-4.13 AS build 2 | 3 | RUN sudo apk add libev-dev gmp-dev 4 | 5 | COPY --chown=opam:opam elastic-apm.opam elastic-apm-logs-reporter.opam elastic-apm-lwt-client.opam elastic-apm-lwt-reporter.opam elastic-apm-rock.opam . 6 | 7 | RUN opam install ./ --deps-only -y 8 | 9 | ENTRYPOINT opam exec -- ocamlc -version 10 | -------------------------------------------------------------------------------- /example/2-database-ocaml/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name hello) 3 | (public_name demo_database) 4 | (libraries 5 | base 6 | opium 7 | pgx_lwt_unix 8 | pgx_value_core 9 | fmt.tty 10 | logs.fmt 11 | elastic-apm-rock) 12 | (preprocess 13 | (pps lwt_ppx ppx_yojson_conv))) 14 | 15 | (env 16 | (default 17 | (flags (:standard))) 18 | (static 19 | (flags 20 | (:standard -ccopt -static)))) 21 | -------------------------------------------------------------------------------- /core/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name elastic_apm) 3 | (preprocess 4 | (pps ppx_yojson_conv)) 5 | (public_name elastic-apm) 6 | (libraries 7 | dune-build-info 8 | mtime.clock.os 9 | ptime.clock.os 10 | unix 11 | yojson 12 | hex 13 | uri)) 14 | 15 | (rule 16 | (targets elastic_apm_generated_sysinfo.ml) 17 | (action 18 | (with-stdout-to 19 | %{targets} 20 | (run ../codegen/system_info.exe)))) 21 | -------------------------------------------------------------------------------- /async_reporter/reporter.mli: -------------------------------------------------------------------------------- 1 | include Elastic_apm.Reporter_intf.S 2 | 3 | module Log : Async.Log.Global_intf 4 | 5 | module Host : sig 6 | type t 7 | 8 | val server_env_key : string 9 | 10 | val token_env_key : string 11 | 12 | val of_env : unit -> t option 13 | 14 | val make : Uri.t -> token:string -> t 15 | end 16 | 17 | val create : 18 | ?client:Blue_http.Client.t -> 19 | ?max_messages_per_batch:int -> 20 | Host.t -> 21 | Elastic_apm.Metadata.t -> 22 | t 23 | -------------------------------------------------------------------------------- /rock_middleware/apm.mli: -------------------------------------------------------------------------------- 1 | module Init : sig 2 | val setup_reporter : 3 | ?host:Elastic_apm_lwt_reporter.Reporter.Host.t -> 4 | ?version:string -> 5 | ?environment:string -> 6 | ?node:string -> 7 | string -> 8 | unit 9 | end 10 | 11 | module Apm_context : sig 12 | val find : Rock.Request.t -> Elastic_apm_lwt_client.Client.context option 13 | 14 | val get : Rock.Request.t -> Elastic_apm_lwt_client.Client.context 15 | end 16 | 17 | val m : Rock.Middleware.t 18 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version = 0.19.0 2 | profile = default 3 | break-cases = all 4 | break-fun-decl = fit-or-vertical 5 | break-infix = fit-or-vertical 6 | cases-exp-indent = 2 7 | if-then-else = k-r 8 | indicate-multiline-delimiters = closing-on-separate-line 9 | leading-nested-match-parens = true 10 | let-and = sparse 11 | module-item-spacing = preserve 12 | parens-ite = true 13 | parens-tuple-patterns = always 14 | parse-docstrings = true 15 | single-case = sparse 16 | type-decl = sparse 17 | wrap-comments = true 18 | let-binding-spacing = double-semicolon 19 | -------------------------------------------------------------------------------- /example/1-hello-opium/.ocamlformat: -------------------------------------------------------------------------------- 1 | version = 0.19.0 2 | profile = default 3 | break-cases = all 4 | break-fun-decl = fit-or-vertical 5 | break-infix = fit-or-vertical 6 | cases-exp-indent = 2 7 | if-then-else = k-r 8 | indicate-multiline-delimiters = closing-on-separate-line 9 | leading-nested-match-parens = true 10 | let-and = sparse 11 | module-item-spacing = preserve 12 | parens-ite = true 13 | parens-tuple-patterns = always 14 | parse-docstrings = true 15 | single-case = sparse 16 | type-decl = sparse 17 | wrap-comments = true 18 | let-binding-spacing = double-semicolon 19 | -------------------------------------------------------------------------------- /example/1-hello-opium/demo_hello_world.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | depends: [ 4 | "dune" {>= "2.9"} 5 | "lwt" 6 | "opium" 7 | "lwt_ppx" 8 | "odoc" {with-doc} 9 | ] 10 | build: [ 11 | ["dune" "subst"] {dev} 12 | [ 13 | "dune" 14 | "build" 15 | "-p" 16 | name 17 | "-j" 18 | jobs 19 | "--promote-install-files=false" 20 | "@install" 21 | "@runtest" {with-test} 22 | "@doc" {with-doc} 23 | ] 24 | ["dune" "install" "-p" name "--create-install-files" name] 25 | ] 26 | -------------------------------------------------------------------------------- /example/2-database-ocaml/.ocamlformat: -------------------------------------------------------------------------------- 1 | version = 0.19.0 2 | profile = default 3 | break-cases = all 4 | break-fun-decl = fit-or-vertical 5 | break-infix = fit-or-vertical 6 | cases-exp-indent = 2 7 | if-then-else = k-r 8 | indicate-multiline-delimiters = closing-on-separate-line 9 | leading-nested-match-parens = true 10 | let-and = sparse 11 | module-item-spacing = preserve 12 | parens-ite = true 13 | parens-tuple-patterns = always 14 | parse-docstrings = true 15 | single-case = sparse 16 | type-decl = sparse 17 | wrap-comments = true 18 | let-binding-spacing = double-semicolon 19 | -------------------------------------------------------------------------------- /example/3-polyglot-services/ocaml/.ocamlformat: -------------------------------------------------------------------------------- 1 | version = 0.19.0 2 | profile = default 3 | break-cases = all 4 | break-fun-decl = fit-or-vertical 5 | break-infix = fit-or-vertical 6 | cases-exp-indent = 2 7 | if-then-else = k-r 8 | indicate-multiline-delimiters = closing-on-separate-line 9 | leading-nested-match-parens = true 10 | let-and = sparse 11 | module-item-spacing = preserve 12 | parens-ite = true 13 | parens-tuple-patterns = always 14 | parse-docstrings = true 15 | single-case = sparse 16 | type-decl = sparse 17 | wrap-comments = true 18 | let-binding-spacing = double-semicolon 19 | -------------------------------------------------------------------------------- /example/3-polyglot-services/ocaml/demo_polyglot.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | depends: [ 4 | "dune" {>= "2.9"} 5 | "lwt" 6 | "opium" 7 | "lwt_ppx" 8 | "odoc" {with-doc} 9 | ] 10 | build: [ 11 | ["dune" "subst"] {dev} 12 | [ 13 | "dune" 14 | "build" 15 | "-p" 16 | name 17 | "-j" 18 | jobs 19 | "--promote-install-files=false" 20 | "@install" 21 | "@runtest" {with-test} 22 | "@doc" {with-doc} 23 | ] 24 | ["dune" "install" "-p" name "--create-install-files" name] 25 | ] 26 | -------------------------------------------------------------------------------- /codegen/system_info.ml: -------------------------------------------------------------------------------- 1 | module C = Configurator.V1 2 | 3 | let optional_var name var = 4 | let var = 5 | match var with 6 | | None -> "None" 7 | | Some var -> Printf.sprintf "Some %S" var 8 | in 9 | Printf.sprintf "let %s = %s" name var 10 | ;; 11 | 12 | let () = 13 | C.main ~name:"elastic-apm.codegen" (fun configurator -> 14 | let arch = C.ocaml_config_var configurator "architecture" in 15 | let system = C.ocaml_config_var configurator "system" in 16 | print_endline (optional_var "architecture" arch); 17 | print_endline (optional_var "platform" system) 18 | ) 19 | ;; 20 | -------------------------------------------------------------------------------- /example/2-database-ocaml/demo_database.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | depends: [ 4 | "dune" {>= "2.9"} 5 | "lwt" 6 | "opium" 7 | "pgx_lwt_unix" 8 | "pgx_value_core" 9 | "lwt_ppx" 10 | "odoc" {with-doc} 11 | ] 12 | build: [ 13 | ["dune" "subst"] {dev} 14 | [ 15 | "dune" 16 | "build" 17 | "-p" 18 | name 19 | "-j" 20 | jobs 21 | "--promote-install-files=false" 22 | "@install" 23 | "@runtest" {with-test} 24 | "@doc" {with-doc} 25 | ] 26 | ["dune" "install" "-p" name "--create-install-files" name] 27 | ] 28 | -------------------------------------------------------------------------------- /core/request.ml: -------------------------------------------------------------------------------- 1 | type t = 2 | | Metadata of Metadata.t 3 | | Transaction of Transaction.t 4 | | Span of Span.t 5 | | Error of Error.t 6 | | Metrics of Metrics.t 7 | 8 | let yojson_of_t payload = 9 | match payload with 10 | | Metadata metadata -> `Assoc [ ("metadata", Metadata.yojson_of_t metadata) ] 11 | | Transaction transaction -> 12 | `Assoc [ ("transaction", Transaction.yojson_of_t transaction) ] 13 | | Error e -> `Assoc [ ("error", Error.yojson_of_t e) ] 14 | | Span span -> `Assoc [ ("span", Span.yojson_of_t span) ] 15 | | Metrics metrics -> `Assoc [ ("metricset", Metrics.yojson_of_t metrics) ] 16 | ;; 17 | -------------------------------------------------------------------------------- /lwt_reporter/lib/reporter.mli: -------------------------------------------------------------------------------- 1 | include Elastic_apm.Reporter_intf.S 2 | 3 | val log_source : Logs.Src.t 4 | (** The log source used for all logging within the lwt apm reporter. This can be 5 | used to control the logging level filter for this library. *) 6 | 7 | module Host : sig 8 | type t 9 | 10 | val server_env_key : string 11 | 12 | val token_env_key : string 13 | 14 | val of_env : unit -> t option 15 | 16 | val make : Uri.t -> token:string -> t 17 | end 18 | 19 | val create : 20 | ?cohttp_ctx:Cohttp_lwt_unix.Client.ctx -> 21 | ?max_messages_per_batch:int -> 22 | Host.t -> 23 | Elastic_apm.Metadata.t -> 24 | t 25 | -------------------------------------------------------------------------------- /lwt_client/client.mli: -------------------------------------------------------------------------------- 1 | include Elastic_apm.Client_intf.S with type 'a io := 'a Lwt.t 2 | 3 | val set_reporter : Elastic_apm_lwt_reporter.Reporter.t option -> unit 4 | 5 | val make_context' : 6 | ?trace_id:Elastic_apm.Id.Trace_id.t -> 7 | ?parent_id:Elastic_apm.Id.Span_id.t -> 8 | ?request:Elastic_apm.Context.Http.Request.t -> 9 | kind:string -> 10 | string -> 11 | context 12 | 13 | val make_context : 14 | ?context:context -> 15 | ?request:Elastic_apm.Context.Http.Request.t -> 16 | kind:string -> 17 | string -> 18 | context 19 | 20 | val init_reporter : 21 | Elastic_apm_lwt_reporter.Reporter.Host.t -> 22 | Elastic_apm.Metadata.Service.t -> 23 | unit 24 | -------------------------------------------------------------------------------- /async_client/client.mli: -------------------------------------------------------------------------------- 1 | include Elastic_apm.Client_intf.S with type 'a io := 'a Async.Deferred.t 2 | 3 | val set_reporter : Elastic_apm_async_reporter.Reporter.t option -> unit 4 | 5 | val make_context' : 6 | ?trace_id:Elastic_apm.Id.Trace_id.t -> 7 | ?parent_id:Elastic_apm.Id.Span_id.t -> 8 | ?request:Elastic_apm.Context.Http.Request.t -> 9 | kind:string -> 10 | string -> 11 | context 12 | 13 | val make_context : 14 | ?context:context -> 15 | ?request:Elastic_apm.Context.Http.Request.t -> 16 | kind:string -> 17 | string -> 18 | context 19 | 20 | val init_reporter : 21 | Elastic_apm_async_reporter.Reporter.Host.t -> 22 | Elastic_apm.Metadata.Service.t -> 23 | unit 24 | -------------------------------------------------------------------------------- /core/system_info.ml: -------------------------------------------------------------------------------- 1 | module Platform = struct 2 | type t = { 3 | architecture : string; 4 | hostname : string; 5 | platform : string; 6 | } 7 | [@@deriving yojson_of] 8 | 9 | let detect () = 10 | let architecture = 11 | Option.value ~default:"Unknown" Elastic_apm_generated_sysinfo.architecture 12 | in 13 | let platform = 14 | Option.value ~default:"Unknown" Elastic_apm_generated_sysinfo.platform 15 | in 16 | let hostname = Unix.gethostname () in 17 | { architecture; hostname; platform } 18 | ;; 19 | 20 | let default = Lazy.from_fun detect 21 | 22 | let make ~architecture ~hostname ~platform = 23 | { architecture; hostname; platform } 24 | ;; 25 | end 26 | -------------------------------------------------------------------------------- /example/3-polyglot-services/python/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | import time 3 | import requests 4 | import os 5 | from elasticapm.contrib.flask import ElasticAPM 6 | 7 | app = Flask(__name__) 8 | apm = ElasticAPM(app) 9 | 10 | @app.route("/", methods=["GET"]) 11 | def index(): 12 | downstream_service = os.environ['DOWNSTREAM_SERVICE'] 13 | r = requests.get(f'{downstream_service}/ping') 14 | return r.text 15 | 16 | @app.route("/hi/", methods=["GET"]) 17 | def hello(name): 18 | return f"Hello, {name}" 19 | 20 | @app.route("/bye/", methods=["GET"]) 21 | def goodbye(name): 22 | return f"Goodbye, {name}!" 23 | 24 | if __name__ == "__main__": 25 | app.run(debug=False, host='0.0.0.0') 26 | -------------------------------------------------------------------------------- /core/context.mli: -------------------------------------------------------------------------------- 1 | module Http : sig 2 | module Url : sig 3 | type t = Uri.t [@@deriving yojson_of] 4 | end 5 | module Request : sig 6 | type t [@@deriving yojson_of] 7 | 8 | val make : 9 | ?body:string -> 10 | ?headers:(string * string) list -> 11 | http_version:string -> 12 | meth:string -> 13 | Uri.t -> 14 | t 15 | 16 | val url : t -> Url.t 17 | end 18 | 19 | module Response : sig 20 | type t [@@deriving yojson_of] 21 | val make : 22 | ?decoded_body_size:int -> 23 | ?encoded_body_size:int -> 24 | ?headers:(string * string) list -> 25 | ?transfer_size:int -> 26 | int -> 27 | t 28 | 29 | val status_code : t -> int 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /core/span.mli: -------------------------------------------------------------------------------- 1 | type http_context = { 2 | url : string; 3 | status_code : int option; [@yojson.option] 4 | } 5 | [@@deriving yojson_of] 6 | 7 | type context = { http : http_context option [@yojson.option] } 8 | [@@deriving yojson_of] 9 | 10 | type t = { 11 | duration : Duration.t; 12 | id : Id.Span_id.t; 13 | name : string; 14 | transaction_id : Id.Span_id.t; 15 | parent_id : Id.Span_id.t; 16 | trace_id : Id.Trace_id.t; 17 | type_ : string; 18 | timestamp : Timestamp.t; 19 | context : context option; 20 | } 21 | [@@deriving yojson_of] 22 | 23 | val make : 24 | ?http_context:http_context -> 25 | duration:Duration.t -> 26 | id:Id.Span_id.t -> 27 | kind:string -> 28 | transaction_id:Id.Span_id.t -> 29 | parent_id:Id.Span_id.t -> 30 | trace_id:Id.Trace_id.t -> 31 | timestamp:Timestamp.t -> 32 | string -> 33 | t 34 | -------------------------------------------------------------------------------- /.github/workflows/build-client.yml: -------------------------------------------------------------------------------- 1 | name: Test and Build OCaml APM Client 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | jobs: 12 | build: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | ocaml-version: 19 | - 4.13.x 20 | 21 | runs-on: ${{ matrix.os }} 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v2 26 | 27 | - name: Use OCaml ${{ matrix.ocaml-version }} 28 | uses: ocaml/setup-ocaml@v2 29 | with: 30 | ocaml-compiler: ${{ matrix.ocaml-version }} 31 | dune-cache: true 32 | 33 | - run: echo "⚡️ Ride the Camel" 34 | 35 | - run: opam install . --deps-only --with-test -y 36 | 37 | - run: opam exec -- dune runtest 38 | -------------------------------------------------------------------------------- /example/1-hello-opium/hello.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Opium 3 | 4 | type message_object = { message : string } [@@deriving yojson] 5 | 6 | let reverse_handler req = 7 | let%lwt body = Body.to_string req.Request.body in 8 | let message_object = 9 | body |> Yojson.Safe.from_string |> message_object_of_yojson 10 | in 11 | 12 | let rev_body = 13 | `String message_object.message |> Yojson.Safe.to_string |> String.rev 14 | in 15 | Response.make ~body:(Body.of_string rev_body) () |> Lwt.return 16 | ;; 17 | 18 | let init () = 19 | Fmt_tty.setup_std_outputs (); 20 | Logs.set_level (Some Info); 21 | Logs.set_reporter (Logs_fmt.reporter ()); 22 | Elastic_apm_rock.Apm.Init.setup_reporter "opium-elastic-apm-demo" 23 | ;; 24 | 25 | let () = 26 | init (); 27 | App.empty 28 | |> App.middleware Elastic_apm_rock.Apm.m 29 | |> App.post "/reverse" reverse_handler 30 | |> App.run_command 31 | ;; 32 | -------------------------------------------------------------------------------- /core/metrics.mli: -------------------------------------------------------------------------------- 1 | module Metric_transaction : sig 2 | type t [@@deriving yojson_of] 3 | 4 | val make : name:string -> type_:string -> t 5 | end 6 | 7 | module Metric_span : sig 8 | type t [@@deriving yojson_of] 9 | 10 | val make : type_:string -> subtype:string -> t 11 | end 12 | 13 | module Metric : sig 14 | type t = 15 | | Histogram of { 16 | counts : int64 list; 17 | values : float list; 18 | } 19 | | Guage of { 20 | value : float; 21 | unit_ : string option; 22 | } 23 | | Counter of { 24 | value : float; 25 | unit_ : string option; 26 | } 27 | [@@deriving yojson_of] 28 | end 29 | 30 | type t [@@deriving yojson_of] 31 | 32 | val create : 33 | ?timestamp:Timestamp.t -> 34 | ?labels:(string * string) list -> 35 | ?metric_span:Metric_span.t -> 36 | ?metric_transaction:Metric_transaction.t -> 37 | samples:(string * Metric.t) list -> 38 | unit -> 39 | t 40 | -------------------------------------------------------------------------------- /core/client_intf.ml: -------------------------------------------------------------------------------- 1 | module type S = sig 2 | type context 3 | type 'a io 4 | 5 | val trace_id : context -> Id.Trace_id.t 6 | 7 | val id : context -> Id.Span_id.t 8 | 9 | val parent_id : context -> Id.Span_id.t 10 | val set_response : context -> Context.Http.Response.t -> unit 11 | 12 | module Transaction : sig 13 | val init : 14 | ?request:Context.Http.Request.t -> 15 | ?context:context -> 16 | kind:string -> 17 | string -> 18 | context 19 | 20 | val close : context -> unit 21 | end 22 | 23 | module Span : sig 24 | val init : context -> kind:string -> string -> context 25 | 26 | val close : context -> unit 27 | end 28 | 29 | val with_transaction : 30 | ?context:context -> 31 | ?request:Context.Http.Request.t -> 32 | kind:string -> 33 | string -> 34 | (context -> 'a io) -> 35 | 'a io 36 | 37 | val with_span : 38 | context -> kind:string -> string -> (context -> 'a io) -> 'a io 39 | end 40 | -------------------------------------------------------------------------------- /logs_reporter/reporter.ml: -------------------------------------------------------------------------------- 1 | type t = { 2 | mutable metadata_logged : bool; 3 | metadata : Elastic_apm.Metadata.t; 4 | src : Logs.src; 5 | log : (module Logs.LOG); 6 | } 7 | 8 | let pp_request ppf request = 9 | let json = Elastic_apm.Request.yojson_of_t request in 10 | Format.fprintf ppf "%s" (Yojson.Safe.to_string json) 11 | ;; 12 | 13 | let push t (request : Elastic_apm.Request.t) = 14 | let module Log = (val t.log) in 15 | if not t.metadata_logged then ( 16 | Log.info (fun m -> m "%a" pp_request (Metadata t.metadata)); 17 | t.metadata_logged <- true 18 | ); 19 | match request with 20 | | (Metadata _ | Span _ | Transaction _ | Metrics _) as req -> 21 | Log.info (fun m -> m "%a" pp_request req) 22 | | Error _ as req -> Log.err (fun m -> m "%a" pp_request req) 23 | ;; 24 | 25 | let create ?(src = Logs.Src.create "elastic-apm.logs-reporter") metadata = 26 | { metadata_logged = false; src; log = Logs.src_log src; metadata } 27 | ;; 28 | 29 | let src t = t.src 30 | -------------------------------------------------------------------------------- /core/span.ml: -------------------------------------------------------------------------------- 1 | type http_context = { 2 | url : string; 3 | status_code : int option; [@yojson.option] 4 | } 5 | [@@deriving yojson_of] 6 | 7 | type context = { http : http_context option [@yojson.option] } 8 | [@@deriving yojson_of] 9 | 10 | type t = { 11 | duration : Duration.t; 12 | id : Id.Span_id.t; 13 | name : string; 14 | transaction_id : Id.Span_id.t; 15 | parent_id : Id.Span_id.t; 16 | trace_id : Id.Trace_id.t; 17 | type_ : string; [@key "type"] 18 | timestamp : Timestamp.t; 19 | context : context option; [@yojson.option] 20 | } 21 | [@@deriving yojson_of] 22 | 23 | let make 24 | ?http_context 25 | ~duration 26 | ~id 27 | ~kind 28 | ~transaction_id 29 | ~parent_id 30 | ~trace_id 31 | ~timestamp 32 | name = 33 | let context = Option.map (fun http -> { http = Some http }) http_context in 34 | { 35 | duration; 36 | id; 37 | name; 38 | transaction_id; 39 | parent_id; 40 | trace_id; 41 | type_ = kind; 42 | timestamp; 43 | context; 44 | } 45 | ;; 46 | -------------------------------------------------------------------------------- /example/2-database-ocaml/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ocaml-base as build 2 | 3 | RUN mkdir /home/opam/dist 4 | RUN mkdir /home/opam/vendor 5 | RUN mkdir /home/opam/dist/lib 6 | WORKDIR /home/opam/dist 7 | COPY --chown=opam:opam example/2-database-ocaml/ . 8 | 9 | RUN opam install ./demo_database.opam --deps-only -y 10 | 11 | COPY --chown=opam:opam codegen vendor/codegen 12 | COPY --chown=opam:opam core vendor/core 13 | COPY --chown=opam:opam logs_reporter vendor/logs_reporter 14 | COPY --chown=opam:opam lwt_client vendor/lwt_client 15 | COPY --chown=opam:opam lwt_reporter vendor/lwt_client 16 | COPY --chown=opam:opam rock_middleware vendor/rock_middleware 17 | COPY --chown=opam:opam dune-project vendor/dune-project 18 | 19 | RUN opam exec -- dune build @install 20 | RUN ldd ./_build/default/hello.exe | grep "=> /" | awk '{print $3}' | xargs -I '{}' cp -v '{}' /home/opam/dist/lib/ 21 | 22 | FROM alpine:3.14 23 | 24 | WORKDIR /dist 25 | COPY --from=build /home/opam/dist/lib /lib/ 26 | COPY --from=build /home/opam/dist/_build/default/hello.exe . 27 | ENTRYPOINT ./hello.exe -p $port 28 | -------------------------------------------------------------------------------- /example/2-database-ocaml/db.ml: -------------------------------------------------------------------------------- 1 | open Base 2 | open Opium 3 | 4 | let db_host = Option.value ~default:"localhost" (Sys.getenv "PG_HOST") 5 | 6 | let create_db_pool size = 7 | Lwt_pool.create size 8 | ~validate:(fun conn -> Pgx_lwt_unix.alive conn) 9 | ~dispose:(fun conn -> Pgx_lwt_unix.close conn) 10 | (fun () -> 11 | Pgx_lwt_unix.connect ~host:db_host ~user:"ocaml_demo" 12 | ~password:"ocaml_demo" ~database:"ocaml_demo" () 13 | ) 14 | ;; 15 | 16 | let key = 17 | Context.Key.create 18 | ( "database_pool", 19 | fun (_ : Pgx_lwt_unix.t Lwt_pool.t) -> Sexplib0.Sexp.Atom "" 20 | ) 21 | ;; 22 | 23 | let m size = 24 | let filter handler ({ Request.env; _ } as req) = 25 | let env = Context.add key (create_db_pool size) env in 26 | let req = { req with env } in 27 | handler req 28 | in 29 | Rock.Middleware.create ~filter ~name:"Setup Database Pool" 30 | ;; 31 | 32 | let with_conn { Request.env; _ } ~f = 33 | let pool = Context.find_exn key env in 34 | Lwt_pool.use pool (fun conn -> f conn) 35 | ;; 36 | -------------------------------------------------------------------------------- /example/1-hello-opium/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ocaml-base as build 2 | 3 | RUN mkdir /home/opam/dist 4 | RUN mkdir /home/opam/vendor 5 | RUN mkdir /home/opam/dist/lib 6 | WORKDIR /home/opam/dist 7 | COPY --chown=opam:opam example/1-hello-opium/ . 8 | 9 | RUN opam install ./demo_hello_world.opam --deps-only -y 10 | 11 | COPY --chown=opam:opam codegen vendor/codegen 12 | COPY --chown=opam:opam core vendor/core 13 | COPY --chown=opam:opam logs_reporter vendor/logs_reporter 14 | COPY --chown=opam:opam lwt_client vendor/lwt_client 15 | COPY --chown=opam:opam lwt_reporter vendor/lwt_client 16 | COPY --chown=opam:opam rock_middleware vendor/rock_middleware 17 | COPY --chown=opam:opam dune-project vendor/dune-project 18 | 19 | 20 | RUN opam exec -- dune build @install 21 | RUN ldd ./_build/default/hello.exe | grep "=> /" | awk '{print $3}' | xargs -I '{}' cp -v '{}' /home/opam/dist/lib/ 22 | 23 | FROM alpine:3.14 24 | 25 | WORKDIR /dist 26 | COPY --from=build /home/opam/dist/lib /lib/ 27 | COPY --from=build /home/opam/dist/_build/default/hello.exe . 28 | ENTRYPOINT ./hello.exe -p $port 29 | -------------------------------------------------------------------------------- /example/3-polyglot-services/ocaml/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ocaml-base as build 2 | 3 | RUN mkdir /home/opam/dist 4 | RUN mkdir /home/opam/vendor 5 | RUN mkdir /home/opam/dist/lib 6 | WORKDIR /home/opam/dist 7 | COPY --chown=opam:opam example/3-polyglot-services/ocaml/ . 8 | 9 | RUN opam install ./demo_polyglot.opam --deps-only -y 10 | 11 | COPY --chown=opam:opam codegen vendor/codegen 12 | COPY --chown=opam:opam core vendor/core 13 | COPY --chown=opam:opam logs_reporter vendor/logs_reporter 14 | COPY --chown=opam:opam lwt_client vendor/lwt_client 15 | COPY --chown=opam:opam lwt_reporter vendor/lwt_client 16 | COPY --chown=opam:opam rock_middleware vendor/rock_middleware 17 | COPY --chown=opam:opam dune-project vendor/dune-project 18 | 19 | 20 | RUN opam exec -- dune build 21 | 22 | RUN ldd ./_build/default/hello.exe | grep "=> /" | awk '{print $3}' | xargs -I '{}' cp -v '{}' ./lib/ 23 | 24 | FROM alpine:3.14 25 | 26 | WORKDIR /dist 27 | COPY --from=build /home/opam/dist/_build/default/hello.exe . 28 | COPY --from=build /home/opam/dist/lib /lib/ 29 | ENTRYPOINT ./hello.exe -p $port 30 | -------------------------------------------------------------------------------- /core/transaction.mli: -------------------------------------------------------------------------------- 1 | module Span_count : sig 2 | type t = { 3 | dropped : int option; 4 | started : int; 5 | } 6 | [@@deriving yojson_of] 7 | 8 | val make : ?dropped:int -> int -> t 9 | 10 | val add_started : t -> int -> t 11 | 12 | val add_dropped : t -> int -> t 13 | end 14 | 15 | type context = { 16 | request : Context.Http.Request.t option; [@yojson.option] 17 | response : Context.Http.Response.t option; [@yojson.option] 18 | } 19 | [@@deriving yojson_of] 20 | 21 | type t = { 22 | timestamp : Timestamp.t; 23 | duration : Duration.t; 24 | id : Id.Span_id.t; 25 | span_count : Span_count.t; 26 | trace_id : Id.Trace_id.t; 27 | parent_id : Id.Span_id.t option; 28 | type_ : string; 29 | name : string; 30 | context : context; 31 | } 32 | [@@deriving yojson_of] 33 | 34 | val make : 35 | ?parent_id:Id.Span_id.t -> 36 | ?request:Context.Http.Request.t -> 37 | ?response:Context.Http.Response.t -> 38 | timestamp:Timestamp.t -> 39 | duration:Duration.t -> 40 | id:Id.Span_id.t -> 41 | span_count:Span_count.t -> 42 | trace_id:Id.Trace_id.t -> 43 | kind:string -> 44 | string -> 45 | t 46 | -------------------------------------------------------------------------------- /elastic-apm-logs-reporter.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | maintainer: [ 4 | "Hezekiah M. Carty " 5 | "Pete Hampton " 6 | "Anurag Soni " 7 | ] 8 | authors: [ 9 | "Adam Ringwood " 10 | "Hezekiah M. Carty " 11 | "Pete Hampton " 12 | "Anurag Soni " 13 | ] 14 | license: "Apache-2.0" 15 | homepage: "https://github.com/elastic/apm-agent-ocaml" 16 | bug-reports: "https://github.com/elastic/apm-agent-ocaml/issues" 17 | depends: [ 18 | "dune" {>= "2.9"} 19 | "ocaml" {>= "4.12.0"} 20 | "logs" 21 | "elastic-apm" {= version} 22 | "odoc" {with-doc} 23 | ] 24 | build: [ 25 | ["dune" "subst"] {dev} 26 | [ 27 | "dune" 28 | "build" 29 | "-p" 30 | name 31 | "-j" 32 | jobs 33 | "--promote-install-files=false" 34 | "@install" 35 | "@runtest" {with-test} 36 | "@doc" {with-doc} 37 | ] 38 | ["dune" "install" "-p" name "--create-install-files" name] 39 | ] 40 | dev-repo: "git+https://github.com/elastic/apm-agent-ocaml.git" 41 | -------------------------------------------------------------------------------- /elastic-apm-lwt-reporter.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | maintainer: [ 4 | "Hezekiah M. Carty " 5 | "Pete Hampton " 6 | "Anurag Soni " 7 | ] 8 | authors: [ 9 | "Adam Ringwood " 10 | "Hezekiah M. Carty " 11 | "Pete Hampton " 12 | "Anurag Soni " 13 | ] 14 | license: "Apache-2.0" 15 | homepage: "https://github.com/elastic/apm-agent-ocaml" 16 | bug-reports: "https://github.com/elastic/apm-agent-ocaml/issues" 17 | depends: [ 18 | "dune" {>= "2.9"} 19 | "ocaml" {>= "4.12.0"} 20 | "lwt" 21 | "cohttp-lwt-unix" 22 | "elastic-apm" {= version} 23 | "odoc" {with-doc} 24 | ] 25 | build: [ 26 | ["dune" "subst"] {dev} 27 | [ 28 | "dune" 29 | "build" 30 | "-p" 31 | name 32 | "-j" 33 | jobs 34 | "--promote-install-files=false" 35 | "@install" 36 | "@runtest" {with-test} 37 | "@doc" {with-doc} 38 | ] 39 | ["dune" "install" "-p" name "--create-install-files" name] 40 | ] 41 | dev-repo: "git+https://github.com/elastic/apm-agent-ocaml.git" 42 | -------------------------------------------------------------------------------- /elastic-apm-lwt-client.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | maintainer: [ 4 | "Hezekiah M. Carty " 5 | "Pete Hampton " 6 | "Anurag Soni " 7 | ] 8 | authors: [ 9 | "Adam Ringwood " 10 | "Hezekiah M. Carty " 11 | "Pete Hampton " 12 | "Anurag Soni " 13 | ] 14 | license: "Apache-2.0" 15 | homepage: "https://github.com/elastic/apm-agent-ocaml" 16 | bug-reports: "https://github.com/elastic/apm-agent-ocaml/issues" 17 | depends: [ 18 | "dune" {>= "2.9"} 19 | "ocaml" {>= "4.12.0"} 20 | "lwt" 21 | "lwt_ppx" 22 | "cohttp-lwt-unix" 23 | "elastic-apm" {= version} 24 | "elastic-apm-lwt-reporter" {= version} 25 | "odoc" {with-doc} 26 | ] 27 | build: [ 28 | ["dune" "subst"] {dev} 29 | [ 30 | "dune" 31 | "build" 32 | "-p" 33 | name 34 | "-j" 35 | jobs 36 | "--promote-install-files=false" 37 | "@install" 38 | "@runtest" {with-test} 39 | "@doc" {with-doc} 40 | ] 41 | ["dune" "install" "-p" name "--create-install-files" name] 42 | ] 43 | dev-repo: "git+https://github.com/elastic/apm-agent-ocaml.git" 44 | -------------------------------------------------------------------------------- /elastic-apm-rock.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Rock middleware for Elastic APM" 4 | description: "Application performance monitoring for rock services" 5 | maintainer: [ 6 | "Hezekiah M. Carty " 7 | "Pete Hampton " 8 | "Anurag Soni " 9 | ] 10 | authors: [ 11 | "Adam Ringwood " 12 | "Hezekiah M. Carty " 13 | "Pete Hampton " 14 | "Anurag Soni " 15 | ] 16 | license: "Apache-2.0" 17 | homepage: "https://github.com/elastic/apm-agent-ocaml" 18 | bug-reports: "https://github.com/elastic/apm-agent-ocaml/issues" 19 | depends: [ 20 | "dune" {>= "2.9"} 21 | "ocaml" {>= "4.12.0"} 22 | "rock" 23 | "dune-build-info" 24 | "rock" 25 | "lwt_ppx" 26 | "elastic-apm-lwt-client" {= version} 27 | "odoc" {with-doc} 28 | ] 29 | build: [ 30 | ["dune" "subst"] {dev} 31 | [ 32 | "dune" 33 | "build" 34 | "-p" 35 | name 36 | "-j" 37 | jobs 38 | "--promote-install-files=false" 39 | "@install" 40 | "@runtest" {with-test} 41 | "@doc" {with-doc} 42 | ] 43 | ["dune" "install" "-p" name "--create-install-files" name] 44 | ] 45 | dev-repo: "git+https://github.com/elastic/apm-agent-ocaml.git" 46 | -------------------------------------------------------------------------------- /elastic-apm-async-reporter.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | maintainer: [ 4 | "Hezekiah M. Carty " 5 | "Pete Hampton " 6 | "Anurag Soni " 7 | ] 8 | authors: [ 9 | "Adam Ringwood " 10 | "Hezekiah M. Carty " 11 | "Pete Hampton " 12 | "Anurag Soni " 13 | ] 14 | license: "Apache-2.0" 15 | homepage: "https://github.com/elastic/apm-agent-ocaml" 16 | bug-reports: "https://github.com/elastic/apm-agent-ocaml/issues" 17 | depends: [ 18 | "dune" {>= "2.9"} 19 | "ocaml" {>= "4.12.0"} 20 | "async" {< "v0.15.0"} 21 | "blue_http" 22 | "elastic-apm" {= version} 23 | "odoc" {with-doc} 24 | ] 25 | build: [ 26 | ["dune" "subst"] {dev} 27 | [ 28 | "dune" 29 | "build" 30 | "-p" 31 | name 32 | "-j" 33 | jobs 34 | "--promote-install-files=false" 35 | "@install" 36 | "@runtest" {with-test} 37 | "@doc" {with-doc} 38 | ] 39 | ["dune" "install" "-p" name "--create-install-files" name] 40 | ] 41 | dev-repo: "git+https://github.com/elastic/apm-agent-ocaml.git" 42 | pin-depends: [ 43 | [ 44 | "blue_http.dev" "git+https://github.com/arenadotio/blue-http.git#1140cf44d0828b7bce27cc1ec133aab471e266d0" 45 | ] 46 | ] 47 | -------------------------------------------------------------------------------- /elastic-apm.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Elastic Application Performance Monitoring (APM) client library" 4 | maintainer: [ 5 | "Hezekiah M. Carty " 6 | "Pete Hampton " 7 | "Anurag Soni " 8 | ] 9 | authors: [ 10 | "Adam Ringwood " 11 | "Hezekiah M. Carty " 12 | "Pete Hampton " 13 | "Anurag Soni " 14 | ] 15 | license: "Apache-2.0" 16 | homepage: "https://github.com/elastic/apm-agent-ocaml" 17 | bug-reports: "https://github.com/elastic/apm-agent-ocaml/issues" 18 | depends: [ 19 | "dune" {>= "2.9"} 20 | "ocaml" {>= "4.12.0"} 21 | "hex" 22 | "mtime" 23 | "dune-configurator" 24 | "dune-build-info" 25 | "ppx_expect" {with-test} 26 | "ppx_yojson_conv" 27 | "yojson" 28 | "ptime" {>= "0.8.5"} 29 | "odoc" {with-doc} 30 | ] 31 | build: [ 32 | ["dune" "subst"] {dev} 33 | [ 34 | "dune" 35 | "build" 36 | "-p" 37 | name 38 | "-j" 39 | jobs 40 | "--promote-install-files=false" 41 | "@install" 42 | "@runtest" {with-test} 43 | "@doc" {with-doc} 44 | ] 45 | ["dune" "install" "-p" name "--create-install-files" name] 46 | ] 47 | dev-repo: "git+https://github.com/elastic/apm-agent-ocaml.git" 48 | -------------------------------------------------------------------------------- /elastic-apm-async-client.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | maintainer: [ 4 | "Hezekiah M. Carty " 5 | "Pete Hampton " 6 | "Anurag Soni " 7 | ] 8 | authors: [ 9 | "Adam Ringwood " 10 | "Hezekiah M. Carty " 11 | "Pete Hampton " 12 | "Anurag Soni " 13 | ] 14 | license: "Apache-2.0" 15 | homepage: "https://github.com/elastic/apm-agent-ocaml" 16 | bug-reports: "https://github.com/elastic/apm-agent-ocaml/issues" 17 | depends: [ 18 | "dune" {>= "2.9"} 19 | "ocaml" {>= "4.12.0"} 20 | "async" 21 | "elastic-apm" {= version} 22 | "elastic-apm-async-reporter" {= version} 23 | "odoc" {with-doc} 24 | ] 25 | build: [ 26 | ["dune" "subst"] {dev} 27 | [ 28 | "dune" 29 | "build" 30 | "-p" 31 | name 32 | "-j" 33 | jobs 34 | "--promote-install-files=false" 35 | "@install" 36 | "@runtest" {with-test} 37 | "@doc" {with-doc} 38 | ] 39 | ["dune" "install" "-p" name "--create-install-files" name] 40 | ] 41 | dev-repo: "git+https://github.com/elastic/apm-agent-ocaml.git" 42 | pin-depends: [ 43 | [ 44 | "blue_http.dev" "git+https://github.com/arenadotio/blue-http.git#1140cf44d0828b7bce27cc1ec133aab471e266d0" 45 | ] 46 | ] 47 | -------------------------------------------------------------------------------- /core/transaction.ml: -------------------------------------------------------------------------------- 1 | module Span_count = struct 2 | type t = { 3 | dropped : int option; [@yojson.option] 4 | started : int; 5 | } 6 | [@@deriving yojson_of] 7 | 8 | let make ?dropped started = { dropped; started } 9 | 10 | let add_started t more = { t with started = t.started + more } 11 | 12 | let add_dropped t more = 13 | match more with 14 | | 0 -> t 15 | | _ -> 16 | let current = Option.value t.dropped ~default:0 in 17 | { t with dropped = Some (current + more) } 18 | ;; 19 | end 20 | 21 | type context = { 22 | request : Context.Http.Request.t option; [@yojson.option] 23 | response : Context.Http.Response.t option; [@yojson.option] 24 | } 25 | [@@deriving yojson_of] 26 | 27 | type t = { 28 | timestamp : Timestamp.t; 29 | duration : Duration.t; 30 | id : Id.Span_id.t; 31 | span_count : Span_count.t; 32 | trace_id : Id.Trace_id.t; 33 | parent_id : Id.Span_id.t option; [@yojson.option] 34 | type_ : string; [@key "type"] 35 | name : string; 36 | context : context; 37 | } 38 | [@@deriving yojson_of] 39 | 40 | let make 41 | ?parent_id 42 | ?request 43 | ?response 44 | ~timestamp 45 | ~duration 46 | ~id 47 | ~span_count 48 | ~trace_id 49 | ~kind 50 | name = 51 | let context = { request; response } in 52 | { 53 | timestamp; 54 | duration; 55 | id; 56 | span_count; 57 | trace_id; 58 | type_ = kind; 59 | name; 60 | parent_id; 61 | context; 62 | } 63 | ;; 64 | -------------------------------------------------------------------------------- /core/id.ml: -------------------------------------------------------------------------------- 1 | let default_rand = Random.State.make_self_init () 2 | 3 | let fill_64_bits state buf ~pos = 4 | assert (Bytes.length buf - pos >= 8); 5 | let a = Random.State.bits state in 6 | let b = Random.State.bits state in 7 | let c = Random.State.bits state in 8 | Bytes.unsafe_set buf pos (Char.chr (a land 0xFF)); 9 | Bytes.unsafe_set buf (pos + 1) (Char.chr ((a lsr 8) land 0xFF)); 10 | Bytes.unsafe_set buf (pos + 2) (Char.chr ((a lsr 16) land 0xFF)); 11 | Bytes.unsafe_set buf (pos + 3) (Char.chr (b land 0xFF)); 12 | Bytes.unsafe_set buf (pos + 4) (Char.chr ((b lsr 8) land 0xFF)); 13 | Bytes.unsafe_set buf (pos + 5) (Char.chr ((b lsr 16) land 0xFF)); 14 | Bytes.unsafe_set buf (pos + 6) (Char.chr (c land 0xFF)); 15 | Bytes.unsafe_set buf (pos + 7) (Char.chr ((c lsr 8) land 0xFF)) 16 | ;; 17 | 18 | let fill_128_bits state buf ~pos = 19 | fill_64_bits state buf ~pos; 20 | fill_64_bits state buf ~pos:(pos + 8) 21 | ;; 22 | 23 | let make_id_module byte_count fill_buffer = 24 | let module M = struct 25 | type t = string 26 | 27 | let equal = String.equal 28 | 29 | let create_gen state = 30 | let b = Bytes.create byte_count in 31 | fill_buffer state b ~pos:0; 32 | Bytes.unsafe_to_string b 33 | ;; 34 | 35 | let create () = create_gen default_rand 36 | 37 | let to_string t = t 38 | 39 | let to_hex t = 40 | match Hex.of_string t with 41 | | `Hex t -> t 42 | ;; 43 | 44 | let of_hex t = Hex.to_string (`Hex t) 45 | 46 | let yojson_of_t t = `String (to_hex t) 47 | end in 48 | (module M : Id_intf.S) 49 | ;; 50 | 51 | module Span_id = (val make_id_module 8 fill_64_bits) 52 | 53 | module Trace_id = (val make_id_module 16 fill_128_bits) 54 | 55 | module Error_id = (val make_id_module 16 fill_128_bits) 56 | -------------------------------------------------------------------------------- /core/metadata.mli: -------------------------------------------------------------------------------- 1 | module Process : sig 2 | type t [@@deriving yojson_of] 3 | 4 | val make : ?parent_process_id:int -> ?argv:string array -> int -> string -> t 5 | 6 | val default : t Lazy.t 7 | end 8 | 9 | module Container : sig 10 | type t [@@deriving yojson_of] 11 | 12 | val make : string -> t 13 | end 14 | 15 | module System : sig 16 | type t [@@deriving yojson_of] 17 | 18 | val make : 19 | ?container:Container.t -> ?system_info:System_info.Platform.t -> unit -> t 20 | end 21 | 22 | module Agent : sig 23 | type t [@@deriving yojson_of] 24 | 25 | val t : t 26 | end 27 | 28 | module Framework : sig 29 | type t [@@deriving yojson_of] 30 | 31 | val make : ?version:string -> string -> t 32 | end 33 | 34 | module Language : sig 35 | type t [@@deriving yojson_of] 36 | 37 | val t : t 38 | end 39 | 40 | module Runtime : sig 41 | type t [@@deriving yojson_of] 42 | 43 | val t : t 44 | end 45 | 46 | module Cloud : sig 47 | type id_with_name = { 48 | id : string; 49 | name : string; 50 | } 51 | [@@deriving yojson_of] 52 | 53 | type t [@@deriving yojson_of] 54 | 55 | val make : 56 | ?region:string -> 57 | ?availability_zone:string -> 58 | ?instance:id_with_name -> 59 | ?machine:string -> 60 | ?account:id_with_name -> 61 | ?project:id_with_name -> 62 | string -> 63 | t 64 | end 65 | 66 | module Service : sig 67 | type t [@@deriving yojson_of] 68 | 69 | val make : 70 | ?version:string -> 71 | ?environment:string -> 72 | ?framework:Framework.t -> 73 | ?node:string -> 74 | string -> 75 | t 76 | end 77 | 78 | module User : sig 79 | type t [@@deriving yojson_of] 80 | 81 | val make : ?username:string -> ?id:string -> ?email:string -> unit -> t 82 | end 83 | 84 | type t [@@deriving yojson_of] 85 | 86 | val make : 87 | ?process:Process.t -> 88 | ?system:System.t -> 89 | ?cloud:Cloud.t -> 90 | ?user:User.t -> 91 | Service.t -> 92 | t 93 | -------------------------------------------------------------------------------- /core/context.ml: -------------------------------------------------------------------------------- 1 | module Http = struct 2 | module Headers = struct 3 | type t = (string * string) list 4 | let yojson_of_t t = 5 | match t with 6 | | [] -> `Null 7 | | xs -> `Assoc ((List.map (fun (k, v) -> (k, `String v))) xs) 8 | ;; 9 | end 10 | 11 | module Url = struct 12 | type payload = { 13 | full : string; 14 | hash : string option; [@yojson.option] 15 | hostname : string option; [@yojson.option] 16 | pathname : string; 17 | port : int option; [@yojson.option] 18 | protocol : string option; [@yojson.option] 19 | } 20 | [@@deriving yojson_of] 21 | 22 | let to_payload uri = 23 | let full = Uri.to_string uri in 24 | let hash = Uri.fragment uri in 25 | let hostname = Uri.host uri in 26 | let pathname = Uri.path uri in 27 | let port = Uri.port uri in 28 | let protocol = Uri.scheme uri in 29 | { full; hash; hostname; pathname; port; protocol } 30 | ;; 31 | 32 | type t = Uri.t 33 | 34 | let yojson_of_t t = yojson_of_payload (to_payload t) 35 | end 36 | module Response = struct 37 | type t = { 38 | decoded_body_size : int option; [@yojson.option] 39 | encoded_body_size : int option; [@yojson.option] 40 | headers : Headers.t; 41 | status_code : int; 42 | transfer_size : int option; [@yojson.option] 43 | } 44 | [@@deriving yojson_of] 45 | 46 | let status_code t = t.status_code 47 | 48 | let make 49 | ?decoded_body_size 50 | ?encoded_body_size 51 | ?(headers = []) 52 | ?transfer_size 53 | status_code = 54 | { 55 | decoded_body_size; 56 | encoded_body_size; 57 | transfer_size; 58 | headers; 59 | status_code; 60 | } 61 | ;; 62 | end 63 | 64 | module Request = struct 65 | type t = { 66 | body : string option; [@yojson.option] 67 | headers : Headers.t; 68 | http_version : string; 69 | meth : string; [@yojson.key "method"] 70 | url : Url.t; 71 | } 72 | [@@deriving yojson_of] 73 | 74 | let url t = t.url 75 | 76 | let make ?body ?(headers = []) ~http_version ~meth url = 77 | { body; headers; http_version; meth; url } 78 | ;; 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OCaml agent for Elastic APM 2 | 3 |

4 | Test and Build OCaml APM Client 7 |

8 | 9 | ## Dev setup 10 | 11 | Clone 12 | 13 | ```bash 14 | git clone git@github.com:elastic/apm-agent-ocaml.git 15 | cd apm-agent-ocaml 16 | ``` 17 | 18 | Setup OCaml environment 19 | 20 | ```bash 21 | opam switch create . 4.13.1 22 | opam install ocamlformat ocamlformat-rpc ocaml-lsp-server 23 | ``` 24 | 25 | You can run a build in watch mode so new changes are automatically detected and 26 | rebuilt. In a terminal, in or outside of your editor: 27 | 28 | ```bash 29 | dune build -w 30 | ``` 31 | 32 | Tests can also be run in watch mode with expectation tests automatically 33 | capturing changes in output! This gives an almost magical experience where test 34 | results update as new tests are written _and_ as the library itself evolves. 35 | 36 | ```bash 37 | dune test -w --auto-promote 38 | ``` 39 | 40 | If you don't have it installed already, watch mode needs `fswatch` which can be 41 | installed via `brew` on macOS: 42 | 43 | ```bash 44 | brew install fswatch 45 | ``` 46 | 47 | If you're using vscode, install the OCaml Platform plugin by OCaml Labs. The 48 | OCaml plugin should automatically detect the local opam switch you just created. 49 | If you setup format on save in the editor it will automatically format new 50 | changes via the LSP server to conform to the project's standard formatting. 51 | 52 | You should now be ready to work on the OCaml Elastic APM agent! 53 | 54 | ### Running the examples locally 55 | 56 | * Install docker + docker compose 57 | * `docker compose build ocaml-base` 58 | * `docker compose build` 59 | * `docker compose up -d` 60 | 61 | Once docker compose up finishes the following endpoints will be available: 62 | 63 | * http://localhost:5601 -> Kibana 64 | * http://localhost:4000 -> [OCaml hello-world example](./example/1-hello-opium) 65 | * http://localhost:4001 -> [OCaml example that talks to a python service](./example/3-polyglot-services/ocaml) 66 | * http://localhost:5000 -> [Python flask application](./example/3-polyglot-services/python) 67 | * http://localhost:4003 -> [OCaml example that talks to postgres](./example/2-database-ocaml) 68 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.9) 2 | 3 | (name elastic-apm) 4 | 5 | (license Apache-2.0) 6 | 7 | (maintainers 8 | "Hezekiah M. Carty " 9 | "Pete Hampton " 10 | "Anurag Soni ") 11 | 12 | (authors 13 | "Adam Ringwood " 14 | "Hezekiah M. Carty " 15 | "Pete Hampton " 16 | "Anurag Soni ") 17 | 18 | (source 19 | (github elastic/apm-agent-ocaml)) 20 | 21 | (generate_opam_files true) 22 | 23 | (package 24 | (name elastic-apm) 25 | (synopsis "Elastic Application Performance Monitoring (APM) client library") 26 | (depends 27 | (ocaml 28 | (>= 4.12.0)) 29 | hex 30 | mtime 31 | dune-configurator 32 | dune-build-info 33 | (ppx_expect :with-test) 34 | ppx_yojson_conv 35 | yojson 36 | (ptime 37 | (>= 0.8.5)))) 38 | 39 | (package 40 | (name elastic-apm-lwt-reporter) 41 | (depends 42 | (ocaml 43 | (>= 4.12.0)) 44 | lwt 45 | cohttp-lwt-unix 46 | (elastic-apm 47 | (= :version)))) 48 | 49 | (package 50 | (name elastic-apm-lwt-client) 51 | (depends 52 | (ocaml 53 | (>= 4.12.0)) 54 | lwt 55 | lwt_ppx 56 | cohttp-lwt-unix 57 | (elastic-apm 58 | (= :version)) 59 | (elastic-apm-lwt-reporter 60 | (= :version)))) 61 | 62 | (package 63 | (name elastic-apm-async-reporter) 64 | (depends 65 | (ocaml 66 | (>= 4.12.0)) 67 | (async 68 | (< v0.15.0)) 69 | blue_http 70 | (elastic-apm 71 | (= :version)))) 72 | 73 | (package 74 | (name elastic-apm-async-client) 75 | (depends 76 | (ocaml 77 | (>= 4.12.0)) 78 | async 79 | (elastic-apm 80 | (= :version)) 81 | (elastic-apm-async-reporter 82 | (= :version)))) 83 | 84 | (package 85 | (name elastic-apm-logs-reporter) 86 | (depends 87 | (ocaml 88 | (>= 4.12.0)) 89 | logs 90 | (elastic-apm 91 | (= :version)))) 92 | 93 | (package 94 | (name elastic-apm-rock) 95 | (synopsis "Rock middleware for Elastic APM") 96 | (description "Application performance monitoring for rock services") 97 | (depends 98 | (ocaml 99 | (>= 4.12.0)) 100 | rock 101 | dune-build-info 102 | rock 103 | lwt_ppx 104 | (elastic-apm-lwt-client 105 | (= :version)))) 106 | -------------------------------------------------------------------------------- /core/error.ml: -------------------------------------------------------------------------------- 1 | module Stack_frame = struct 2 | type t = { 3 | filename : string option; [@yojson.option] 4 | lineno : int option; [@yojson.option] 5 | function_ : string option; [@key "function"] [@yojson.option] 6 | colno : int option; [@yojson.option] 7 | } 8 | [@@deriving yojson_of] 9 | 10 | let make slot = 11 | let function_ = Printexc.Slot.name slot in 12 | let (lineno, colno, filename) = 13 | match Printexc.Slot.location slot with 14 | | Some loc -> 15 | (Some loc.line_number, Some loc.start_char, Some loc.filename) 16 | | None -> (None, None, None) 17 | in 18 | { filename; lineno; function_; colno } 19 | ;; 20 | end 21 | 22 | module Exception = struct 23 | type t = { 24 | message : string; 25 | type_ : string; [@key "type"] 26 | stacktrace : Stack_frame.t array option; [@yojson.option] 27 | } 28 | [@@deriving yojson_of] 29 | 30 | let concat_map t ~f = Array.concat (Array.to_list (Array.map f t)) 31 | 32 | let make backtrace exn = 33 | let message = Printexc.to_string exn in 34 | let backtrace_entries = Printexc.raw_backtrace_entries backtrace in 35 | let stacktrace = 36 | concat_map backtrace_entries ~f:(fun raw_backtrace_entry -> 37 | Option.value ~default:[||] 38 | (Printexc.backtrace_slots_of_raw_entry raw_backtrace_entry) 39 | ) 40 | |> Array.map Stack_frame.make 41 | in 42 | { 43 | message; 44 | stacktrace = 45 | ( if Array.length stacktrace = 0 then 46 | None 47 | else 48 | Some stacktrace 49 | ); 50 | type_ = "exn"; 51 | } 52 | ;; 53 | end 54 | 55 | type t = { 56 | id : Id.Error_id.t; 57 | timestamp : Timestamp.t; 58 | trace_id : Id.Trace_id.t option; [@yojson.option] 59 | transaction_id : Id.Span_id.t option; [@yojson.option] 60 | parent_id : Id.Span_id.t option; [@yojson.option] 61 | exception_ : Exception.t; [@key "exception"] 62 | } 63 | [@@deriving yojson_of] 64 | 65 | let make 66 | ?random_state 67 | ?trace_id 68 | ?transaction_id 69 | ?parent_id 70 | ~exn 71 | ~backtrace 72 | ~timestamp 73 | () = 74 | let id = 75 | match random_state with 76 | | None -> Id.Error_id.create () 77 | | Some state -> Id.Error_id.create_gen state 78 | in 79 | { 80 | id; 81 | timestamp; 82 | trace_id; 83 | transaction_id; 84 | parent_id; 85 | exception_ = Exception.make backtrace exn; 86 | } 87 | ;; 88 | -------------------------------------------------------------------------------- /example/3-polyglot-services/ocaml/hello.ml: -------------------------------------------------------------------------------- 1 | open! Base 2 | open Opium 3 | open Lwt 4 | open Cohttp_lwt_unix 5 | 6 | let upstream_service = Uri.of_string (Sys.getenv_exn "UPSTREAM_SERVICE") 7 | 8 | type message_object = { name : string } [@@deriving yojson] 9 | 10 | let ping_handler _req = Opium.Response.of_plain_text "pong" |> Lwt.return 11 | 12 | let upstream_handler req = 13 | let apm_ctx = Elastic_apm_rock.Apm.Apm_context.get req in 14 | let id = Elastic_apm_lwt_client.Client.trace_id apm_ctx in 15 | let parent_id = Elastic_apm_lwt_client.Client.id apm_ctx in 16 | let traceparent = 17 | Printf.sprintf "00-%s-%s-01" 18 | (Elastic_apm.Id.Trace_id.to_hex id) 19 | (Elastic_apm.Id.Span_id.to_hex parent_id) 20 | in 21 | let headers = Cohttp.Header.of_list [ ("traceparent", traceparent) ] in 22 | Client.get ~headers upstream_service >>= fun (_resp, body) -> 23 | body |> Cohttp_lwt.Body.to_string >|= fun body -> 24 | Opium.Response.of_plain_text body 25 | ;; 26 | 27 | let upstream_handler_greet req = 28 | let greet = Router.param req "greet" in 29 | let name = Router.param req "name" in 30 | let url = 31 | Uri.with_path upstream_service (String.concat ~sep:"/" [ greet; name ]) 32 | in 33 | let apm_ctx = Elastic_apm_rock.Apm.Apm_context.get req in 34 | Elastic_apm_lwt_client.Client.with_span apm_ctx ~kind:"http" 35 | "fetch greeting from upstream" (fun ctx -> 36 | let trace_id = Elastic_apm_lwt_client.Client.trace_id ctx in 37 | let parent_id = Elastic_apm_lwt_client.Client.id ctx in 38 | let traceparent = 39 | Printf.sprintf "00-%s-%s-01" 40 | (Elastic_apm.Id.Trace_id.to_hex trace_id) 41 | (Elastic_apm.Id.Span_id.to_hex parent_id) 42 | in 43 | let headers = Cohttp.Header.of_list [ ("traceparent", traceparent) ] in 44 | Client.get ~headers url 45 | ) 46 | >>= fun (_resp, body) -> 47 | body |> Cohttp_lwt.Body.to_string >|= fun body -> 48 | Opium.Response.of_plain_text body 49 | ;; 50 | 51 | let init () = 52 | Fmt_tty.setup_std_outputs (); 53 | Logs.set_reporter (Logs_fmt.reporter ()); 54 | Logs.set_level (Some Debug); 55 | Elastic_apm_rock.Apm.Init.setup_reporter "elastic-apm-opium-example-polyglot" 56 | ;; 57 | 58 | let () = 59 | init (); 60 | App.empty 61 | |> App.middleware Elastic_apm_rock.Apm.m 62 | |> App.get "/ping" ping_handler 63 | |> App.get "/upstream" upstream_handler 64 | |> App.get "/upstream/:greet/:name" upstream_handler_greet 65 | |> App.run_command 66 | ;; 67 | -------------------------------------------------------------------------------- /example/2-database-ocaml/hello.ml: -------------------------------------------------------------------------------- 1 | open! Core_kernel 2 | open Opium 3 | open Lwt.Infix 4 | 5 | type uuid = Uuidm.t 6 | 7 | let yojson_of_uuid t = `String (Uuidm.to_string t) 8 | 9 | type timestamp = Time.t 10 | 11 | let yojson_of_timestamp t = `String (Time.to_string t) 12 | 13 | type message = { 14 | id : uuid; 15 | message : string; 16 | created_at : timestamp; 17 | } 18 | [@@deriving yojson_of] 19 | 20 | type payload = { messages : message list } [@@deriving yojson_of] 21 | 22 | let healthcheck _req = Lwt.return (Response.of_plain_text "") 23 | 24 | let fetch_messages req = 25 | let apm_ctx = Elastic_apm_rock.Apm.Apm_context.get req in 26 | Elastic_apm_lwt_client.Client.with_span apm_ctx ~kind:"db" "postgres lookup" 27 | (fun _ctx -> 28 | Db.with_conn req ~f:(fun conn -> 29 | Pgx_lwt_unix.execute_fold conn 30 | "SELECT id, message, created_at from message ORDER BY created_at \ 31 | DESC" 32 | ~init:[] ~f:(fun acc row -> 33 | match row with 34 | | [ id; message; created_at ] -> 35 | let id = Pgx.Value.to_uuid_exn id in 36 | let message = Pgx.Value.to_string_exn message in 37 | let created_at = Pgx_value_core.to_time_exn created_at in 38 | Lwt.return ({ id; message; created_at } :: acc) 39 | | _ -> failwith "Unexpected response from database" 40 | ) 41 | ) 42 | ) 43 | ;; 44 | 45 | let get_messages req = 46 | fetch_messages req >|= fun messages -> 47 | Response.of_json (yojson_of_payload { messages }) 48 | ;; 49 | 50 | let insert_message req = 51 | Body.to_string req.Request.body >>= fun body -> 52 | let apm_ctx = Elastic_apm_rock.Apm.Apm_context.get req in 53 | Elastic_apm_lwt_client.Client.with_span apm_ctx ~kind:"db" "postgres insert" 54 | (fun _ctx -> 55 | Db.with_conn req ~f:(fun conn -> 56 | Pgx_lwt_unix.execute_unit 57 | ~params:[ Pgx.Value.of_string body ] 58 | conn "INSERT INTO message (message) VALUES ($1)" 59 | ) 60 | ) 61 | >>= fun () -> get_messages req 62 | ;; 63 | 64 | let init () = 65 | Fmt_tty.setup_std_outputs (); 66 | Logs.set_reporter (Logs_fmt.reporter ()); 67 | Logs.set_level (Some Info); 68 | Elastic_apm_rock.Apm.Init.setup_reporter "elastic-apm-opium-example-database" 69 | ;; 70 | 71 | let () = 72 | init (); 73 | App.empty 74 | |> App.middleware Elastic_apm_rock.Apm.m 75 | |> App.middleware (Db.m 10) 76 | |> App.middleware Middleware.logger 77 | |> App.get "/healthcheck" healthcheck 78 | |> App.get "/messages" get_messages 79 | |> App.post "/message" insert_message 80 | |> App.run_command 81 | ;; 82 | -------------------------------------------------------------------------------- /async_reporter/reporter.ml: -------------------------------------------------------------------------------- 1 | open Core 2 | open Async 3 | 4 | module Log = Log.Make_global () 5 | 6 | module Host = struct 7 | type t = { 8 | server : Uri.t; 9 | token : string; 10 | } 11 | 12 | let server_env_key = "ELASTIC_APM_SERVER_URL" 13 | let token_env_key = "ELASTIC_APM_SECRET_TOKEN" 14 | 15 | let make server ~token = { server; token } 16 | 17 | let of_env () = 18 | let%bind.Option server = Sys.getenv server_env_key in 19 | let%bind.Option token = Sys.getenv token_env_key in 20 | Some (make (Uri.of_string server) ~token) 21 | ;; 22 | end 23 | 24 | module Spec = struct 25 | type t = { 26 | metadata : Elastic_apm.Metadata.t; 27 | max_messages_per_batch : int; 28 | client : Blue_http.Client.t; 29 | host : Host.t; 30 | } 31 | 32 | let make 33 | ?(client = Blue_http.Client.create ()) 34 | ?(max_messages_per_batch = 20) 35 | host 36 | metadata = 37 | { metadata; max_messages_per_batch; client; host } 38 | ;; 39 | 40 | let to_headers t = 41 | let headers = [ ("content-type", "application/x-ndjson") ] in 42 | let auth = `Other (Printf.sprintf "Bearer %s" t.host.token) in 43 | Cohttp.Header.add_authorization (Cohttp.Header.of_list headers) auth 44 | ;; 45 | end 46 | 47 | type t = Elastic_apm.Request.t Pipe.Writer.t 48 | 49 | let make_body events = 50 | let jsons = 51 | List.map events ~f:(fun e -> 52 | Yojson.Safe.to_string (Elastic_apm.Request.yojson_of_t e) 53 | ) 54 | in 55 | String.concat ~sep:"\n" jsons 56 | ;; 57 | 58 | let send_events (spec : Spec.t) headers events = 59 | Log.debug "Sending events"; 60 | let uri = Uri.with_path spec.host.server "/intake/v2/events" in 61 | let body = Cohttp_async.Body.of_string (make_body events) in 62 | let%map resp = 63 | Blue_http.call_ignore_body ~client:spec.client ~headers ~body `POST uri 64 | in 65 | Log.debug "Response code: %d" 66 | (Cohttp.Code.code_of_status (Cohttp.Response.status resp)) 67 | ;; 68 | 69 | let read spec reader = 70 | let headers = Spec.to_headers spec in 71 | Deferred.repeat_until_finished () (fun () -> 72 | match%bind 73 | Pipe.read' ~max_queue_length:spec.max_messages_per_batch reader 74 | with 75 | | `Eof -> return (`Finished ()) 76 | | `Ok queue -> 77 | let requests = Queue.to_list queue in 78 | let payload = Elastic_apm.Request.Metadata spec.metadata :: requests in 79 | ( match%map 80 | Monitor.try_with (fun () -> send_events spec headers payload) 81 | with 82 | | Ok () -> `Repeat () 83 | | Error exn -> 84 | Log.error 85 | !"Error while pushing events to APM server: %{sexp: Exn.t}" 86 | exn; 87 | `Repeat () 88 | ) 89 | ) 90 | ;; 91 | 92 | let create ?client ?max_messages_per_batch host metadata = 93 | let spec = Spec.make ?client ?max_messages_per_batch host metadata in 94 | Pipe.create_writer (read spec) 95 | ;; 96 | 97 | let push t event = 98 | if Pipe.is_closed t then 99 | Log.error "No events will be pushed as the APM reporter is down"; 100 | Pipe.write_without_pushback_if_open t event 101 | ;; 102 | -------------------------------------------------------------------------------- /lwt_reporter/lib/reporter.ml: -------------------------------------------------------------------------------- 1 | open Lwt.Infix 2 | 3 | let log_source = Logs.Src.create "elastic_apm.lwt_reporter" 4 | 5 | module Log = (val Logs.src_log log_source) 6 | 7 | module Host = struct 8 | type t = { 9 | server : Uri.t; 10 | token : string; 11 | } 12 | 13 | let server_env_key = "ELASTIC_APM_SERVER_URL" 14 | let token_env_key = "ELASTIC_APM_SECRET_TOKEN" 15 | 16 | let of_env () = 17 | match (Sys.getenv_opt server_env_key, Sys.getenv_opt token_env_key) with 18 | | (None, _) 19 | | (_, None) -> 20 | None 21 | | (Some server, Some token) -> Some { server = Uri.of_string server; token } 22 | ;; 23 | 24 | let make server ~token = { server; token } 25 | end 26 | 27 | type t = { 28 | metadata : Elastic_apm.Metadata.t; 29 | events : Elastic_apm.Request.t Lwt_stream.t; 30 | push : Elastic_apm.Request.t option -> unit; 31 | max_messages_per_batch : int; 32 | host : Host.t; 33 | cohttp_ctx : Cohttp_lwt_unix.Client.ctx option; 34 | } 35 | 36 | let make_headers t = 37 | let headers = [ ("content-type", "application/x-ndjson") ] in 38 | let auth = `Other (Printf.sprintf "Bearer %s" t.host.token) in 39 | Cohttp.Header.add_authorization (Cohttp.Header.of_list headers) auth 40 | ;; 41 | 42 | let make_body events = 43 | let jsons = 44 | List.map 45 | (fun e -> Yojson.Safe.to_string (Elastic_apm.Request.yojson_of_t e)) 46 | events 47 | in 48 | String.concat "\n" jsons 49 | ;; 50 | 51 | let send_events t headers events = 52 | Log.debug (fun m -> m "Sending events"); 53 | let uri = Uri.with_path t.host.server "/intake/v2/events" in 54 | let body = Cohttp_lwt.Body.of_string (make_body events) in 55 | Cohttp_lwt_unix.Client.post ?ctx:t.cohttp_ctx ~headers ~body uri 56 | >>= fun (resp, body) -> 57 | Log.debug (fun m -> m "Response: %a" Cohttp.Response.pp_hum resp); 58 | Cohttp_lwt.Body.to_string body >|= fun body -> 59 | Log.debug (fun m -> m "Body %s" body) 60 | ;; 61 | 62 | let start_reporter t = 63 | let headers = make_headers t in 64 | let rec loop () = 65 | ( Lwt_stream.peek t.events >|= fun _ -> 66 | Lwt_stream.get_available_up_to t.max_messages_per_batch t.events 67 | ) 68 | >>= function 69 | | [] -> loop () 70 | | requests -> 71 | let payload = Elastic_apm.Request.Metadata t.metadata :: requests in 72 | send_events t headers payload >>= fun () -> loop () 73 | in 74 | 75 | Lwt.async (fun () -> 76 | Lwt.catch 77 | (fun () -> 78 | Log.debug (fun m -> m "starting loop"); 79 | loop () 80 | ) 81 | (fun exn -> 82 | Log.err (fun m -> 83 | m "Exception in the reporter loop: %S" (Printexc.to_string exn) 84 | ); 85 | loop () 86 | ) 87 | ) 88 | ;; 89 | 90 | let create ?cohttp_ctx ?(max_messages_per_batch = 20) host metadata = 91 | let (stream, push) = Lwt_stream.create () in 92 | let t = 93 | { 94 | metadata; 95 | events = stream; 96 | push; 97 | max_messages_per_batch; 98 | host; 99 | cohttp_ctx; 100 | } 101 | in 102 | start_reporter t; 103 | t 104 | ;; 105 | 106 | let push t event = 107 | Log.debug (fun m -> m "Pushing events"); 108 | t.push (Some event) 109 | ;; 110 | -------------------------------------------------------------------------------- /core/metrics.ml: -------------------------------------------------------------------------------- 1 | module Metric_transaction = struct 2 | type t = { 3 | type_ : string; [@key "type"] 4 | name : string; 5 | } 6 | [@@deriving yojson_of] 7 | 8 | let make ~name ~type_ = { name; type_ } 9 | end 10 | 11 | module Metric_span = struct 12 | type t = { 13 | type_ : string; [@key "type"] 14 | subtype : string; 15 | } 16 | [@@deriving yojson_of] 17 | 18 | let make ~type_ ~subtype = { type_; subtype } 19 | end 20 | 21 | module Metric = struct 22 | type json = { 23 | type_ : string; [@key "type"] 24 | unit_ : string option; [@key "unit"] [@yojson.option] 25 | value : float option; [@yojson.option] 26 | values : float list option; [@yojson.option] 27 | counts : int64 list option; [@yojson.option] 28 | } 29 | [@@deriving yojson_of] 30 | 31 | type t = 32 | | Histogram of { 33 | counts : int64 list; 34 | values : float list; 35 | } 36 | | Guage of { 37 | value : float; 38 | unit_ : string option; 39 | } 40 | | Counter of { 41 | value : float; 42 | unit_ : string option; 43 | } 44 | 45 | let yojson_of_t t = 46 | let payload = 47 | match t with 48 | | Guage { value; unit_ } -> 49 | { 50 | type_ = "guage"; 51 | unit_; 52 | value = Some value; 53 | values = None; 54 | counts = None; 55 | } 56 | | Counter { value; unit_ } -> 57 | { 58 | type_ = "counter"; 59 | unit_; 60 | value = Some value; 61 | values = None; 62 | counts = None; 63 | } 64 | | Histogram { counts; values } -> 65 | { 66 | type_ = "histogram"; 67 | unit_ = None; 68 | value = None; 69 | values = Some values; 70 | counts = Some counts; 71 | } 72 | in 73 | yojson_of_json payload 74 | ;; 75 | end 76 | 77 | module StringMap = Map.Make (String) 78 | 79 | type labels = string StringMap.t 80 | 81 | let yojson_of_labels labels = 82 | `Assoc 83 | (labels 84 | |> StringMap.to_seq 85 | |> Seq.map (fun (k, v) -> (k, `String v)) 86 | |> List.of_seq 87 | ) 88 | ;; 89 | 90 | type samples = Metric.t StringMap.t 91 | 92 | let yojson_of_samples samples = 93 | `Assoc 94 | (samples 95 | |> StringMap.to_seq 96 | |> Seq.map (fun (k, v) -> (k, Metric.yojson_of_t v)) 97 | |> List.of_seq 98 | ) 99 | ;; 100 | 101 | type t = { 102 | timestamp : Timestamp.t; 103 | labels : labels; 104 | samples : samples; 105 | span : Metric_span.t option; [@yojson.option] 106 | transaction : Metric_transaction.t option; [@yojson.option] 107 | } 108 | [@@deriving yojson_of] 109 | 110 | let create 111 | ?(timestamp = Timestamp.now ()) 112 | ?(labels = []) 113 | ?metric_span 114 | ?metric_transaction 115 | ~samples 116 | () = 117 | match samples with 118 | | [] -> invalid_arg "Can't create metrics without any samples" 119 | | samples -> 120 | { 121 | timestamp; 122 | span = metric_span; 123 | transaction = metric_transaction; 124 | samples = StringMap.of_seq (List.to_seq samples); 125 | labels = StringMap.of_seq (List.to_seq labels); 126 | } 127 | ;; 128 | -------------------------------------------------------------------------------- /rock_middleware/apm.ml: -------------------------------------------------------------------------------- 1 | module Init = struct 2 | let setup_reporter ?host ?version ?environment ?node service_name = 3 | let open Elastic_apm in 4 | let service = 5 | let framework_version = 6 | match Build_info.V1.Statically_linked_libraries.find ~name:"rock" with 7 | | None -> None 8 | | Some library -> 9 | Option.map Build_info.V1.Version.to_string 10 | (Build_info.V1.Statically_linked_library.version library) 11 | in 12 | Metadata.Service.make ?version ?environment ?node 13 | ~framework:(Metadata.Framework.make ?version:framework_version "Rock") 14 | service_name 15 | in 16 | let module Reporter = Elastic_apm_lwt_reporter.Reporter in 17 | let host = 18 | match host with 19 | | None -> Reporter.Host.of_env () 20 | | Some _ as h -> h 21 | in 22 | match host with 23 | | None -> 24 | Logs.warn (fun m -> 25 | m 26 | "APM reporting disabled because %s and %s are not defined in the \ 27 | environment" 28 | Reporter.Host.server_env_key Reporter.Host.token_env_key 29 | ) 30 | | Some host -> 31 | let reporter = 32 | let metadata = Elastic_apm.Metadata.make service in 33 | Elastic_apm_lwt_reporter.Reporter.create host metadata 34 | in 35 | Elastic_apm_lwt_client.Client.set_reporter (Some reporter) 36 | ;; 37 | end 38 | 39 | module Apm_context = struct 40 | let sexp_of_apm_context (_ : Elastic_apm_lwt_client.Client.context) : 41 | Sexplib0.Sexp.t = 42 | Atom "" 43 | ;; 44 | 45 | let key = Rock.Context.Key.create ("elastic-apm-context", sexp_of_apm_context) 46 | 47 | let pp_request ppf (req : Rock.Request.t) = 48 | Fmt.pf ppf "%a %s" Httpaf.Method.pp_hum req.meth req.target 49 | ;; 50 | 51 | let find (req : Rock.Request.t) = Rock.Context.find key req.env 52 | 53 | let get req = 54 | match find req with 55 | | Some apm -> apm 56 | | None -> 57 | Fmt.invalid_arg "APM context is not available in request environment %a" 58 | pp_request req 59 | ;; 60 | end 61 | 62 | let m : Rock.Middleware.t = 63 | let filter handler (req : Rock.Request.t) = 64 | let meth = Httpaf.Method.to_string req.meth in 65 | let path = req.target |> Uri.of_string |> Uri.path in 66 | let name = Fmt.str "%s %s" meth path in 67 | let parent_id = 68 | match Httpaf.Headers.get req.headers "traceparent" with 69 | | None -> None 70 | | Some traceparent -> 71 | ( match String.split_on_char '-' traceparent with 72 | | [ _version; trace_id; parent_id; _flags ] -> 73 | let trace_id = Elastic_apm.Id.Trace_id.of_hex trace_id in 74 | let parent_id = Elastic_apm.Id.Span_id.of_hex parent_id in 75 | Some (trace_id, parent_id) 76 | | _ -> None 77 | ) 78 | in 79 | let ctx = 80 | Option.map 81 | (fun (trace_id, parent_id) -> 82 | Elastic_apm_lwt_client.Client.make_context' ~trace_id ~parent_id 83 | ~kind:"http" name 84 | ) 85 | parent_id 86 | in 87 | let request = 88 | Elastic_apm.Context.Http.Request.make 89 | ~headers:(Httpaf.Headers.to_list req.headers) 90 | ~http_version:(Httpaf.Version.to_string req.version) 91 | ~meth (Uri.of_string req.target) 92 | in 93 | Elastic_apm_lwt_client.Client.with_transaction ~request ?context:ctx 94 | ~kind:"http" name (fun apm -> 95 | let env = Rock.Context.add Apm_context.key apm req.env in 96 | let req = { req with env } in 97 | let%lwt rock_response = handler req in 98 | let response = 99 | Elastic_apm.Context.Http.Response.make 100 | ~headers:(Httpaf.Headers.to_list rock_response.Rock.Response.headers) 101 | (Httpaf.Status.to_code rock_response.status) 102 | in 103 | Elastic_apm_lwt_client.Client.set_response apm response; 104 | Lwt.return rock_response 105 | ) 106 | in 107 | Rock.Middleware.create ~filter ~name:"Elastic APM" 108 | ;; 109 | -------------------------------------------------------------------------------- /test/logs_reporter_output.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | (* Setup log output *) 3 | Fmt_tty.setup_std_outputs (); 4 | Logs.set_reporter (Logs_fmt.reporter ()); 5 | Logs.set_level ~all:true (Some Info) 6 | ;; 7 | 8 | let state = Random.State.make [| 1; 2; 3; 4; 5 |] 9 | 10 | let service = Elastic_apm.Metadata.Service.make "testservice" 11 | let process = Elastic_apm.Metadata.Process.make 1 "testprocess" 12 | let system_info = 13 | Elastic_apm.System_info.Platform.make ~architecture:"testarch" 14 | ~hostname:"testhost" ~platform:"testplatform" 15 | ;; 16 | let system = Elastic_apm.Metadata.System.make ~system_info () 17 | let metadata = Elastic_apm.Metadata.make ~system ~process service 18 | 19 | let trace_id = Elastic_apm.Id.Trace_id.create_gen state 20 | let transaction = 21 | let open Elastic_apm in 22 | Transaction.make 23 | ~timestamp:(Timestamp.of_us_since_epoch (365 * 50 * 86_4000 * 1_000_000)) 24 | ~duration:(Duration.of_span @@ Mtime.Span.of_uint64_ns 80000000L) 25 | ~id:(Id.Span_id.create_gen state) 26 | ~span_count:(Transaction.Span_count.make 1) 27 | ~trace_id ~kind:"request" "testtransaction" 28 | ;; 29 | 30 | let%expect_test "logs reporter - default logs src" = 31 | let reporter = Elastic_apm_logs_reporter.Reporter.create metadata in 32 | Elastic_apm_logs_reporter.Reporter.push reporter (Transaction transaction); 33 | [%expect 34 | {| 35 | inline_test_runner_apm_agent_tests.exe: [INFO] {"metadata":{"process":{"pid":1,"title":"testprocess","argv":[]},"service":{"name":"testservice","agent":{"name":"OCaml","version":"n/a"},"language":{"name":"OCaml","version":"4.13.1"},"runtime":{"name":"OCaml","version":"4.13.1"}},"system":{"architecture":"testarch","hostname":"testhost","platform":"testplatform"}}} 36 | inline_test_runner_apm_agent_tests.exe: [INFO] {"transaction":{"timestamp":15768000000000000,"duration":80.0,"id":"3e466abbf8b38218","span_count":{"started":1},"trace_id":"5e00cc610bf958d233ad4932f4e954cc","type":"request","name":"testtransaction","context":{}}}|}]; 37 | (* No metadata on following log output *) 38 | Elastic_apm_logs_reporter.Reporter.push reporter (Transaction transaction); 39 | [%expect 40 | {| inline_test_runner_apm_agent_tests.exe: [INFO] {"transaction":{"timestamp":15768000000000000,"duration":80.0,"id":"3e466abbf8b38218","span_count":{"started":1},"trace_id":"5e00cc610bf958d233ad4932f4e954cc","type":"request","name":"testtransaction","context":{}}} |}] 41 | ;; 42 | 43 | let%expect_test "logs reporter - custom logs src" = 44 | (* Setup custom log source *) 45 | let src = Logs.Src.create "test.src" in 46 | let reporter = Elastic_apm_logs_reporter.Reporter.create ~src metadata in 47 | (* First log output - has metadata *) 48 | Elastic_apm_logs_reporter.Reporter.push reporter (Transaction transaction); 49 | [%expect 50 | {| 51 | inline_test_runner_apm_agent_tests.exe: [INFO] {"metadata":{"process":{"pid":1,"title":"testprocess","argv":[]},"service":{"name":"testservice","agent":{"name":"OCaml","version":"n/a"},"language":{"name":"OCaml","version":"4.13.1"},"runtime":{"name":"OCaml","version":"4.13.1"}},"system":{"architecture":"testarch","hostname":"testhost","platform":"testplatform"}}} 52 | inline_test_runner_apm_agent_tests.exe: [INFO] {"transaction":{"timestamp":15768000000000000,"duration":80.0,"id":"3e466abbf8b38218","span_count":{"started":1},"trace_id":"5e00cc610bf958d233ad4932f4e954cc","type":"request","name":"testtransaction","context":{}}} |}]; 53 | (* No metadata on following log output *) 54 | Elastic_apm_logs_reporter.Reporter.push reporter (Transaction transaction); 55 | [%expect 56 | {| inline_test_runner_apm_agent_tests.exe: [INFO] {"transaction":{"timestamp":15768000000000000,"duration":80.0,"id":"3e466abbf8b38218","span_count":{"started":1},"trace_id":"5e00cc610bf958d233ad4932f4e954cc","type":"request","name":"testtransaction","context":{}}} |}]; 57 | (* Disable log output or set level too high - no output *) 58 | Logs.Src.set_level (Elastic_apm_logs_reporter.Reporter.src reporter) None; 59 | Elastic_apm_logs_reporter.Reporter.push reporter (Transaction transaction); 60 | [%expect {||}]; 61 | Logs.Src.set_level 62 | (Elastic_apm_logs_reporter.Reporter.src reporter) 63 | (Some Warning); 64 | Elastic_apm_logs_reporter.Reporter.push reporter (Transaction transaction); 65 | [%expect {||}]; 66 | (* Set level low enough - output *) 67 | Logs.Src.set_level 68 | (Elastic_apm_logs_reporter.Reporter.src reporter) 69 | (Some Debug); 70 | Elastic_apm_logs_reporter.Reporter.push reporter (Transaction transaction); 71 | [%expect 72 | {| inline_test_runner_apm_agent_tests.exe: [INFO] {"transaction":{"timestamp":15768000000000000,"duration":80.0,"id":"3e466abbf8b38218","span_count":{"started":1},"trace_id":"5e00cc610bf958d233ad4932f4e954cc","type":"request","name":"testtransaction","context":{}}} |}] 73 | ;; 74 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '2.2' 3 | services: 4 | apm-server: 5 | image: docker.elastic.co/apm/apm-server:7.15.1 6 | depends_on: 7 | elasticsearch: 8 | condition: service_healthy 9 | kibana: 10 | condition: service_healthy 11 | cap_add: ["CHOWN", "DAC_OVERRIDE", "SETGID", "SETUID"] 12 | cap_drop: ["ALL"] 13 | ports: 14 | - 8200:8200 15 | command: > 16 | apm-server -e 17 | -E apm-server.rum.enabled=true 18 | -E setup.kibana.host=kibana:5601 19 | -E setup.template.settings.index.number_of_replicas=0 20 | -E apm-server.kibana.enabled=true 21 | -E apm-server.kibana.host=kibana:5601 22 | -E output.elasticsearch.hosts=["elasticsearch:9200"] 23 | healthcheck: 24 | interval: 10s 25 | retries: 12 26 | test: curl --write-out 'HTTP %{http_code}' --fail --silent --output /dev/null http://localhost:8200/ 27 | 28 | elasticsearch: 29 | image: docker.elastic.co/elasticsearch/elasticsearch:7.15.1 30 | environment: 31 | - bootstrap.memory_lock=true 32 | - cluster.name=docker-cluster 33 | - cluster.routing.allocation.disk.threshold_enabled=false 34 | - discovery.type=single-node 35 | - ES_JAVA_OPTS=-XX:UseAVX=2 -Xms1g -Xmx1g 36 | ulimits: 37 | memlock: 38 | hard: -1 39 | soft: -1 40 | volumes: 41 | - esdata:/usr/share/elasticsearch/data 42 | ports: 43 | - 9200:9200 44 | healthcheck: 45 | interval: 20s 46 | retries: 10 47 | test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"' 48 | 49 | kibana: 50 | image: docker.elastic.co/kibana/kibana:7.15.1 51 | depends_on: 52 | elasticsearch: 53 | condition: service_healthy 54 | environment: 55 | ELASTICSEARCH_URL: http://elasticsearch:9200 56 | ELASTICSEARCH_HOSTS: http://elasticsearch:9200 57 | ports: 58 | - 5601:5601 59 | healthcheck: 60 | interval: 10s 61 | retries: 20 62 | test: curl --write-out 'HTTP %{http_code}' --fail --silent --output /dev/null http://localhost:5601/api/status 63 | 64 | ocaml-base: 65 | image: ocaml-base:latest 66 | build: 67 | context: . 68 | dockerfile: ocaml-base.Dockerfile 69 | 70 | reverse-string-example: 71 | image: reverse-string-ocaml 72 | build: 73 | context: . 74 | dockerfile: example/1-hello-opium/Dockerfile 75 | environment: 76 | port: 4000 77 | ELASTIC_APM_SERVER_URL: "http://apm-server:8200" 78 | ELASTIC_APM_SECRET_TOKEN: "" 79 | expose: 80 | - 4000 81 | ports: 82 | - 4000:4000 83 | depends_on: 84 | apm-server: 85 | condition: service_healthy 86 | 87 | polyglot-example-ocaml: 88 | image: polyglot-example-ocaml 89 | build: 90 | context: . 91 | dockerfile: example/3-polyglot-services/ocaml/Dockerfile 92 | environment: 93 | port: 4001 94 | ELASTIC_APM_SERVER_URL: "http://apm-server:8200" 95 | ELASTIC_APM_SECRET_TOKEN: "" 96 | UPSTREAM_SERVICE: "http://polyglot-example-python:5000" 97 | expose: 98 | - 4001 99 | ports: 100 | - 4001:4001 101 | depends_on: 102 | apm-server: 103 | condition: service_healthy 104 | 105 | polyglot-example-python: 106 | image: polyglot-example-python 107 | build: 108 | context: . 109 | dockerfile: example/3-polyglot-services/python/Dockerfile 110 | environment: 111 | ELASTIC_APM_SERVICE_NAME: "python-ping-pong" 112 | ELASTIC_APM_SERVER_URL: "http://apm-server:8200" 113 | ELASTIC_APM_SECRET_TOKEN: "" 114 | DOWNSTREAM_SERVICE: "http://polyglot-example-ocaml:4001" 115 | expose: 116 | - 5000 117 | ports: 118 | - 5000:5000 119 | depends_on: 120 | apm-server: 121 | condition: service_healthy 122 | 123 | test-postgres: 124 | image: postgres 125 | container_name: test-postgres 126 | environment: 127 | - POSTGRES_USER=ocaml_demo 128 | - POSTGRES_DB=ocaml_demo 129 | - POSTGRES_HOST_AUTH_METHOD=trust 130 | ports: 131 | - 5432:5432 132 | healthcheck: 133 | test: ["CMD-SHELL", "pg_isready"] 134 | interval: 10s 135 | timeout: 5s 136 | retries: 5 137 | volumes: 138 | - ./postgres_init.sql:/docker-entrypoint-initdb.d/postgres_init.sql 139 | 140 | database-example-ocaml: 141 | image: database-example-ocaml 142 | build: 143 | context: . 144 | dockerfile: example/2-database-ocaml/Dockerfile 145 | environment: 146 | port: 4003 147 | ELASTIC_APM_SERVER_URL: "http://apm-server:8200" 148 | ELASTIC_APM_SECRET_TOKEN: "" 149 | PG_HOST: "test-postgres" 150 | expose: 151 | - 4003 152 | ports: 153 | - 4003:4003 154 | depends_on: 155 | apm-server: 156 | condition: service_healthy 157 | test-postgres: 158 | condition: service_healthy 159 | 160 | volumes: 161 | esdata: 162 | driver: local 163 | -------------------------------------------------------------------------------- /core/metadata.ml: -------------------------------------------------------------------------------- 1 | module Process = struct 2 | type t = { 3 | pid : int; 4 | title : string; 5 | parent_process_id : int option; [@key "ppid"] [@yojson.option] 6 | argv : string array; 7 | } 8 | [@@deriving yojson_of] 9 | 10 | let make ?parent_process_id ?(argv = [||]) pid title = 11 | { pid; title; parent_process_id; argv } 12 | ;; 13 | 14 | let default = 15 | lazy 16 | (let pid = Unix.getpid () in 17 | let parent_process_id = 18 | if Sys.win32 then 19 | None 20 | else 21 | Some (Unix.getppid ()) 22 | in 23 | let argv = Sys.argv in 24 | make ?parent_process_id ~argv pid Sys.executable_name 25 | ) 26 | ;; 27 | end 28 | 29 | module Container = struct 30 | type t = { id : string } [@@deriving yojson_of] 31 | 32 | let make id = { id } 33 | end 34 | 35 | module System = struct 36 | type t = { 37 | architecture : string; 38 | hostname : string; 39 | platform : string; 40 | container : Container.t option; [@yojson.option] 41 | } 42 | [@@deriving yojson_of] 43 | 44 | let make 45 | ?container 46 | ?(system_info = Lazy.force System_info.Platform.default) 47 | () = 48 | { 49 | architecture = system_info.architecture; 50 | hostname = system_info.hostname; 51 | platform = system_info.platform; 52 | container; 53 | } 54 | ;; 55 | end 56 | 57 | module Agent = struct 58 | type t = { 59 | name : string; 60 | version : string; 61 | } 62 | [@@deriving yojson_of] 63 | 64 | let t = 65 | let name = "OCaml" in 66 | let version = 67 | match Build_info.V1.version () with 68 | | None -> "n/a" 69 | | Some v -> Build_info.V1.Version.to_string v 70 | in 71 | { name; version } 72 | ;; 73 | end 74 | 75 | module Framework = struct 76 | type t = { 77 | name : string; 78 | version : string option; [@yojson.option] 79 | } 80 | [@@deriving yojson_of] 81 | 82 | let make ?version name = { name; version } 83 | end 84 | 85 | module Language = struct 86 | type t = { 87 | name : string; 88 | version : string; 89 | } 90 | [@@deriving yojson_of] 91 | 92 | let t = { name = "OCaml"; version = Sys.ocaml_version } 93 | end 94 | 95 | module Runtime = struct 96 | type t = { 97 | name : string; 98 | version : string; 99 | } 100 | [@@deriving yojson_of] 101 | 102 | let t = { name = "OCaml"; version = Sys.ocaml_version } 103 | end 104 | 105 | module Cloud = struct 106 | type id_with_name = { 107 | id : string; 108 | name : string; 109 | } 110 | [@@deriving yojson_of] 111 | 112 | type machine = { type_ : string [@key "type"] } [@@deriving yojson_of] 113 | 114 | type t = { 115 | provider : string; 116 | region : string option; [@yojson.option] 117 | availability_zone : string option; [@yojson.option] 118 | instance : id_with_name option; [@yojson.option] 119 | machine : machine option; [@yojson.option] 120 | account : id_with_name option; [@yojson.option] 121 | project : id_with_name option; [@yojson.option] 122 | } 123 | [@@deriving yojson_of] 124 | 125 | let make 126 | ?region 127 | ?availability_zone 128 | ?instance 129 | ?machine 130 | ?account 131 | ?project 132 | provider = 133 | { 134 | provider; 135 | region; 136 | availability_zone; 137 | instance; 138 | machine = Option.map (fun machine -> { type_ = machine }) machine; 139 | account; 140 | project; 141 | } 142 | ;; 143 | end 144 | 145 | module Service = struct 146 | type service_node = { configured_name : string } [@@deriving yojson_of] 147 | 148 | type t = { 149 | name : string; 150 | version : string option; [@yojson.option] 151 | environment : string option; [@yojson.option] 152 | agent : Agent.t; 153 | framework : Framework.t option; [@yojson.option] 154 | language : Language.t option; [@yojson.option] 155 | runtime : Runtime.t option; [@yojson.option] 156 | node : service_node option; [@yojson.option] 157 | } 158 | [@@deriving yojson_of] 159 | 160 | let make ?version ?environment ?framework ?node name = 161 | { 162 | name; 163 | version; 164 | environment; 165 | agent = Agent.t; 166 | framework; 167 | language = Some Language.t; 168 | runtime = Some Runtime.t; 169 | node = Option.map (fun configured_name -> { configured_name }) node; 170 | } 171 | ;; 172 | end 173 | 174 | module User = struct 175 | type t = { 176 | username : string option; [@yojson.option] 177 | id : string option; [@yojson.option] 178 | email : string option; [@yojson.option] 179 | } 180 | [@@deriving yojson_of] 181 | 182 | let is_none { username; id; email } = 183 | match (username, id, email) with 184 | | (None, None, None) -> true 185 | | _ -> false 186 | ;; 187 | 188 | let yojson_of_t t = 189 | if is_none t then 190 | `Null 191 | else 192 | yojson_of_t t 193 | ;; 194 | 195 | let make ?username ?id ?email () = { username; id; email } 196 | end 197 | 198 | type t = { 199 | cloud : Cloud.t option; [@yojson.option] 200 | process : Process.t option; [@yojson.option] 201 | service : Service.t; 202 | system : System.t option; [@yojson.option] 203 | user : User.t option; [@yojson.option] 204 | } 205 | [@@deriving yojson_of] 206 | 207 | let make 208 | ?(process = Lazy.force Process.default) 209 | ?(system = System.make ()) 210 | ?cloud 211 | ?user 212 | service = 213 | { process = Some process; system = Some system; cloud; service; user } 214 | ;; 215 | -------------------------------------------------------------------------------- /lwt_client/client.ml: -------------------------------------------------------------------------------- 1 | open Elastic_apm 2 | open Elastic_apm_lwt_reporter 3 | 4 | module Global_state = struct 5 | let reporter : Reporter.t option ref = ref None 6 | 7 | let random_state : Random.State.t ref = ref (Random.State.make_self_init ()) 8 | 9 | let push request = 10 | Option.iter (fun rep -> Reporter.push rep request) !reporter 11 | ;; 12 | end 13 | 14 | let set_reporter reporter = Global_state.reporter := reporter 15 | 16 | type context = { 17 | request : Context.Http.Request.t option; 18 | mutable response : Context.Http.Response.t option; 19 | timestamp : Timestamp.t; 20 | start : Mtime.t; 21 | id : Id.Span_id.t; 22 | transaction_id : Id.Span_id.t; 23 | parent_id : Id.Span_id.t; 24 | trace_id : Id.Trace_id.t; 25 | kind : string; 26 | name : string; 27 | mutable span_count : Transaction.Span_count.t; 28 | } 29 | 30 | let set_response t response = t.response <- Some response 31 | 32 | let trace_id ctx = ctx.trace_id 33 | let id ctx = ctx.id 34 | let parent_id ctx = ctx.parent_id 35 | 36 | let make_context' ?trace_id ?parent_id ?request ~kind name = 37 | let timestamp = Timestamp.now () in 38 | let start = Mtime_clock.now () in 39 | let id = Id.Span_id.create_gen !Global_state.random_state in 40 | let parent_id = 41 | match parent_id with 42 | | None -> id 43 | | Some parent_id -> parent_id 44 | in 45 | let trace_id = 46 | match trace_id with 47 | | None -> Id.Trace_id.create_gen !Global_state.random_state 48 | | Some trace_id -> trace_id 49 | in 50 | let transaction_id = id in 51 | { 52 | request; 53 | response = None; 54 | timestamp; 55 | start; 56 | id; 57 | transaction_id; 58 | parent_id; 59 | trace_id; 60 | kind; 61 | name; 62 | span_count = Transaction.Span_count.make 0; 63 | } 64 | ;; 65 | 66 | let make_context ?context ?request ~kind name = 67 | let timestamp = Timestamp.now () in 68 | let start = Mtime_clock.now () in 69 | let id = Id.Span_id.create_gen !Global_state.random_state in 70 | let parent_id = 71 | match context with 72 | | None -> id 73 | | Some ctx -> ctx.id 74 | in 75 | let trace_id = 76 | match context with 77 | | None -> Id.Trace_id.create_gen !Global_state.random_state 78 | | Some ctx -> ctx.trace_id 79 | in 80 | let transaction_id = 81 | match context with 82 | | None -> id 83 | | Some ctx -> ctx.transaction_id 84 | in 85 | let request = 86 | match (request, context) with 87 | | (Some request, _) -> Some request 88 | | (None, Some context) -> context.request 89 | | (None, None) -> None 90 | in 91 | { 92 | request; 93 | response = None; 94 | timestamp; 95 | start; 96 | id; 97 | transaction_id; 98 | parent_id; 99 | trace_id; 100 | kind; 101 | name; 102 | span_count = Transaction.Span_count.make 0; 103 | } 104 | ;; 105 | 106 | module Transaction = struct 107 | let init ?request ?context ~kind name = 108 | make_context ?request ?context ~kind name 109 | ;; 110 | 111 | let close context = 112 | let finish = Mtime_clock.now () in 113 | let duration = Mtime.span context.start finish |> Duration.of_span in 114 | let parent_id = 115 | if Id.Span_id.equal context.id context.parent_id then 116 | None 117 | else 118 | Some context.parent_id 119 | in 120 | let transaction = 121 | Transaction.make ?request:context.request ?response:context.response 122 | ?parent_id ~timestamp:context.timestamp ~duration ~id:context.id 123 | ~span_count:context.span_count ~trace_id:context.trace_id 124 | ~kind:context.kind context.name 125 | in 126 | Global_state.push (Transaction transaction) 127 | ;; 128 | end 129 | 130 | module Span = struct 131 | let init context ~kind name = make_context ~context ~kind name 132 | 133 | let close context = 134 | let finish = Mtime_clock.now () in 135 | let duration = Mtime.span context.start finish |> Duration.of_span in 136 | let span = 137 | let http_context = 138 | match (context.request, context.response) with 139 | | (Some request, Some response) -> 140 | Some 141 | { 142 | Span.url = Uri.to_string (Context.Http.Request.url request); 143 | status_code = Some (Context.Http.Response.status_code response); 144 | } 145 | | (Some request, None) -> 146 | Some 147 | { 148 | Span.url = Uri.to_string (Context.Http.Request.url request); 149 | status_code = None; 150 | } 151 | | _ -> None 152 | in 153 | Span.make ?http_context ~duration ~id:context.id ~kind:context.kind 154 | ~transaction_id:context.transaction_id ~parent_id:context.parent_id 155 | ~trace_id:context.trace_id ~timestamp:context.timestamp context.name 156 | in 157 | Global_state.push (Span span) 158 | ;; 159 | end 160 | 161 | let report_exn f context = 162 | match%lwt f context with 163 | | result -> Lwt.return result 164 | | exception exn -> 165 | let backtrace = Printexc.get_raw_backtrace () in 166 | let err = 167 | Error.make ~random_state:!Global_state.random_state 168 | ~trace_id:context.trace_id ~backtrace ~exn 169 | ~timestamp:(Elastic_apm.Timestamp.now ()) 170 | ~parent_id:context.id ~transaction_id:context.transaction_id () 171 | in 172 | Global_state.push (Error err); 173 | raise exn 174 | ;; 175 | 176 | let with_transaction ?context ?request ~kind name f = 177 | let context = Transaction.init ?request ?context ~kind name in 178 | (report_exn f context) [%lwt.finally Lwt.return (Transaction.close context)] 179 | ;; 180 | 181 | let with_span context ~kind name f = 182 | let context = Span.init context ~kind name in 183 | context.span_count <- 184 | Elastic_apm.Transaction.Span_count.add_started context.span_count 1; 185 | (report_exn f context) [%lwt.finally Lwt.return (Span.close context)] 186 | ;; 187 | 188 | let init_reporter host service = 189 | let reporter = 190 | let metadata = Elastic_apm.Metadata.make service in 191 | Elastic_apm_lwt_reporter.Reporter.create host metadata 192 | in 193 | set_reporter (Some reporter) 194 | ;; 195 | -------------------------------------------------------------------------------- /async_client/client.ml: -------------------------------------------------------------------------------- 1 | open Core 2 | open Async 3 | 4 | open Elastic_apm 5 | open Elastic_apm_async_reporter 6 | 7 | module Global_state = struct 8 | let reporter : Reporter.t option ref = ref None 9 | 10 | (* Using Caml.Random here to match the core Elastic_apm module types *) 11 | let random_state : Caml.Random.State.t ref = 12 | ref (Caml.Random.State.make_self_init ()) 13 | ;; 14 | 15 | let push request = 16 | Option.iter !reporter ~f:(fun rep -> Reporter.push rep request) 17 | ;; 18 | end 19 | 20 | let set_reporter reporter = Global_state.reporter := reporter 21 | 22 | type context = { 23 | request : Context.Http.Request.t option; 24 | mutable response : Context.Http.Response.t option; 25 | timestamp : Timestamp.t; 26 | start : Mtime.t; 27 | id : Id.Span_id.t; 28 | transaction_id : Id.Span_id.t; 29 | parent_id : Id.Span_id.t; 30 | trace_id : Id.Trace_id.t; 31 | kind : string; 32 | name : string; 33 | mutable span_count : Transaction.Span_count.t; 34 | } 35 | 36 | let set_response t response = t.response <- Some response 37 | 38 | let trace_id ctx = ctx.trace_id 39 | let id ctx = ctx.id 40 | let parent_id ctx = ctx.parent_id 41 | 42 | let make_context' ?trace_id ?parent_id ?request ~kind name = 43 | let timestamp = Timestamp.now () in 44 | let start = Mtime_clock.now () in 45 | let id = Id.Span_id.create_gen !Global_state.random_state in 46 | let parent_id = 47 | match parent_id with 48 | | None -> id 49 | | Some parent_id -> parent_id 50 | in 51 | let trace_id = 52 | match trace_id with 53 | | None -> Id.Trace_id.create_gen !Global_state.random_state 54 | | Some trace_id -> trace_id 55 | in 56 | let transaction_id = id in 57 | { 58 | request; 59 | response = None; 60 | timestamp; 61 | start; 62 | id; 63 | transaction_id; 64 | parent_id; 65 | trace_id; 66 | kind; 67 | name; 68 | span_count = Transaction.Span_count.make 0; 69 | } 70 | ;; 71 | 72 | let make_context ?context ?request ~kind name = 73 | let timestamp = Timestamp.now () in 74 | let start = Mtime_clock.now () in 75 | let id = Id.Span_id.create_gen !Global_state.random_state in 76 | let parent_id = 77 | match context with 78 | | None -> id 79 | | Some ctx -> ctx.id 80 | in 81 | let trace_id = 82 | match context with 83 | | None -> Id.Trace_id.create_gen !Global_state.random_state 84 | | Some ctx -> ctx.trace_id 85 | in 86 | let transaction_id = 87 | match context with 88 | | None -> id 89 | | Some ctx -> ctx.transaction_id 90 | in 91 | let request = 92 | match (request, context) with 93 | | (Some request, _) -> Some request 94 | | (None, Some context) -> context.request 95 | | (None, None) -> None 96 | in 97 | { 98 | request; 99 | response = None; 100 | timestamp; 101 | start; 102 | id; 103 | transaction_id; 104 | parent_id; 105 | trace_id; 106 | kind; 107 | name; 108 | span_count = Transaction.Span_count.make 0; 109 | } 110 | ;; 111 | 112 | module Transaction = struct 113 | let init ?request ?context ~kind name = 114 | make_context ?request ?context ~kind name 115 | ;; 116 | 117 | let close context = 118 | let finish = Mtime_clock.now () in 119 | let duration = Mtime.span context.start finish |> Duration.of_span in 120 | let parent_id = 121 | if Id.Span_id.equal context.id context.parent_id then 122 | None 123 | else 124 | Some context.parent_id 125 | in 126 | let transaction = 127 | Transaction.make ?request:context.request ?response:context.response 128 | ?parent_id ~timestamp:context.timestamp ~duration ~id:context.id 129 | ~span_count:context.span_count ~trace_id:context.trace_id 130 | ~kind:context.kind context.name 131 | in 132 | Global_state.push (Transaction transaction) 133 | ;; 134 | end 135 | 136 | module Span = struct 137 | let init context ~kind name = make_context ~context ~kind name 138 | 139 | let close context = 140 | let finish = Mtime_clock.now () in 141 | let duration = Mtime.span context.start finish |> Duration.of_span in 142 | let span = 143 | let http_context = 144 | match (context.request, context.response) with 145 | | (Some request, Some response) -> 146 | Some 147 | { 148 | Span.url = Uri.to_string (Context.Http.Request.url request); 149 | status_code = Some (Context.Http.Response.status_code response); 150 | } 151 | | (Some request, None) -> 152 | Some 153 | { 154 | Span.url = Uri.to_string (Context.Http.Request.url request); 155 | status_code = None; 156 | } 157 | | _ -> None 158 | in 159 | Span.make ?http_context ~duration ~id:context.id ~kind:context.kind 160 | ~transaction_id:context.transaction_id ~parent_id:context.parent_id 161 | ~trace_id:context.trace_id ~timestamp:context.timestamp context.name 162 | in 163 | Global_state.push (Span span) 164 | ;; 165 | end 166 | 167 | let report_exn f context = 168 | match%bind Monitor.try_with (fun () -> f context) with 169 | | Ok result -> Deferred.return result 170 | | Error exn -> 171 | let backtrace = Caml.Printexc.get_raw_backtrace () in 172 | let err = 173 | Error.make ~random_state:!Global_state.random_state 174 | ~trace_id:context.trace_id ~backtrace ~exn 175 | ~timestamp:(Elastic_apm.Timestamp.now ()) 176 | ~parent_id:context.id ~transaction_id:context.transaction_id () 177 | in 178 | Global_state.push (Error err); 179 | Caml.Printexc.raise_with_backtrace exn backtrace 180 | ;; 181 | 182 | let with_transaction ?context ?request ~kind name f = 183 | let context = Transaction.init ?request ?context ~kind name in 184 | Monitor.protect 185 | (fun () -> report_exn f context) 186 | ~finally:(fun () -> Deferred.return (Transaction.close context)) 187 | ;; 188 | 189 | let with_span context ~kind name f = 190 | let context = Span.init context ~kind name in 191 | context.span_count <- 192 | Elastic_apm.Transaction.Span_count.add_started context.span_count 1; 193 | Monitor.protect 194 | (fun () -> report_exn f context) 195 | ~finally:(fun () -> Deferred.return (Span.close context)) 196 | ;; 197 | 198 | let init_reporter host service = 199 | let reporter = 200 | let metadata = Elastic_apm.Metadata.make service in 201 | Elastic_apm_async_reporter.Reporter.create host metadata 202 | in 203 | set_reporter (Some reporter) 204 | ;; 205 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Elastic 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /test/json_serialization.ml: -------------------------------------------------------------------------------- 1 | open Elastic_apm 2 | 3 | exception Dummy_exn of string 4 | 5 | let boom () = raise (Dummy_exn "Hello") 6 | 7 | let foo () = boom () 8 | 9 | let test_error () = foo () 10 | 11 | let state = Random.State.make [| 1; 2; 3; 4; 5 |] 12 | 13 | let print_json json = 14 | let pp ppf json = Yojson.Safe.pretty_print ppf json in 15 | Format.fprintf Format.std_formatter "%a@." pp json 16 | ;; 17 | 18 | let%expect_test "duration" = 19 | print_json (Duration.of_span Mtime.Span.one |> Duration.yojson_of_t); 20 | [%expect {| 1e-06 |}] 21 | ;; 22 | 23 | let%expect_test "span id" = 24 | print_json (Id.Span_id.create_gen state |> Id.Span_id.yojson_of_t); 25 | [%expect {| "5e00cc610bf958d2" |}] 26 | ;; 27 | 28 | let%expect_test "trace id" = 29 | print_json (Id.Trace_id.create_gen state |> Id.Trace_id.yojson_of_t); 30 | [%expect {| "33ad4932f4e954cc3e466abbf8b38218" |}] 31 | ;; 32 | 33 | let process = 34 | Metadata.Process.make ~parent_process_id:1 ~argv:[| "hello"; "world" |] 2 35 | "process.exe" 36 | ;; 37 | 38 | let%expect_test "metadata - process" = 39 | let _default : Metadata.Process.t = Lazy.force Metadata.Process.default in 40 | print_json (Metadata.Process.yojson_of_t process); 41 | [%expect 42 | {| 43 | { "pid": 2, "title": "process.exe", "ppid": 1, "argv": [ "hello", "world" ] } |}]; 44 | let process = 45 | Metadata.Process.make ~argv:[| "hello"; "world" |] 2 "process.exe" 46 | in 47 | print_json (Metadata.Process.yojson_of_t process); 48 | [%expect 49 | {| 50 | { "pid": 2, "title": "process.exe", "argv": [ "hello", "world" ] } |}] 51 | ;; 52 | 53 | let container = Metadata.Container.make "hiimacontainer" 54 | 55 | let%expect_test "metadata - container" = 56 | print_json (Metadata.Container.yojson_of_t container); 57 | [%expect {| { "id": "hiimacontainer" } |}] 58 | ;; 59 | 60 | let system = 61 | let system_info = 62 | System_info.Platform.make ~platform:"testplatform" ~hostname:"testhost" 63 | ~architecture:"256bit" 64 | in 65 | Metadata.System.make ~container ~system_info () 66 | ;; 67 | 68 | let%expect_test "metadata - system" = 69 | print_json (Metadata.System.yojson_of_t system); 70 | [%expect 71 | {| 72 | { 73 | "architecture": "256bit", 74 | "hostname": "testhost", 75 | "platform": "testplatform", 76 | "container": { "id": "hiimacontainer" } 77 | } |}]; 78 | let system_info = 79 | System_info.Platform.make ~platform:"testplatform" ~hostname:"testhost" 80 | ~architecture:"256bit" 81 | in 82 | let system = Metadata.System.make ~system_info () in 83 | print_json (Metadata.System.yojson_of_t system); 84 | [%expect 85 | {| 86 | { 87 | "architecture": "256bit", 88 | "hostname": "testhost", 89 | "platform": "testplatform" 90 | } |}] 91 | ;; 92 | 93 | let%expect_test "metadata - agent" = 94 | print_json (Metadata.Agent.yojson_of_t Metadata.Agent.t); 95 | [%expect {| { "name": "OCaml", "version": "n/a" } |}] 96 | ;; 97 | 98 | let framework = Metadata.Framework.make ~version:"beta" "frame" 99 | 100 | let%expect_test "metadata - framework" = 101 | print_json (Metadata.Framework.yojson_of_t framework); 102 | [%expect {| { "name": "frame", "version": "beta" } |}]; 103 | let framework = Metadata.Framework.make "frame" in 104 | print_json (Metadata.Framework.yojson_of_t framework); 105 | [%expect {| { "name": "frame" } |}] 106 | ;; 107 | 108 | let language = Metadata.Language.t 109 | 110 | let%expect_test "metadata - language" = 111 | print_json (Metadata.Language.yojson_of_t language); 112 | [%expect {| { "name": "OCaml", "version": "4.13.1" } |}] 113 | ;; 114 | 115 | let runtime = Metadata.Runtime.t 116 | 117 | let%expect_test "metadata - runtime" = 118 | print_json (Metadata.Runtime.yojson_of_t runtime); 119 | [%expect {| { "name": "OCaml", "version": "4.13.1" } |}] 120 | ;; 121 | 122 | let cloud = 123 | let id : Metadata.Cloud.id_with_name = { id = "012"; name = "abc" } in 124 | Metadata.Cloud.make ~region:"reg" ~availability_zone:"az" ~instance:id 125 | ~machine:"fast" ~account:id ~project:id "name" 126 | ;; 127 | 128 | let%expect_test "metadata - cloud" = 129 | print_json (Metadata.Cloud.yojson_of_t cloud); 130 | [%expect 131 | {| 132 | { 133 | "provider": "name", 134 | "region": "reg", 135 | "availability_zone": "az", 136 | "instance": { "id": "012", "name": "abc" }, 137 | "machine": { "type": "fast" }, 138 | "account": { "id": "012", "name": "abc" }, 139 | "project": { "id": "012", "name": "abc" } 140 | } |}]; 141 | let cloud = Metadata.Cloud.make "name" in 142 | print_json (Metadata.Cloud.yojson_of_t cloud); 143 | [%expect {| 144 | { "provider": "name" } |}] 145 | ;; 146 | 147 | let%expect_test "metadata - service" = 148 | let service = 149 | Metadata.Service.make ~version:"v1" ~environment:"env" ~framework 150 | ~node:"central" "universal" 151 | in 152 | print_json (Metadata.Service.yojson_of_t service); 153 | [%expect 154 | {| 155 | { 156 | "name": "universal", 157 | "version": "v1", 158 | "environment": "env", 159 | "agent": { "name": "OCaml", "version": "n/a" }, 160 | "framework": { "name": "frame", "version": "beta" }, 161 | "language": { "name": "OCaml", "version": "4.13.1" }, 162 | "runtime": { "name": "OCaml", "version": "4.13.1" }, 163 | "node": { "configured_name": "central" } 164 | } |}]; 165 | let service = Metadata.Service.make "universal" in 166 | print_json (Metadata.Service.yojson_of_t service); 167 | [%expect 168 | {| 169 | { 170 | "name": "universal", 171 | "agent": { "name": "OCaml", "version": "n/a" }, 172 | "language": { "name": "OCaml", "version": "4.13.1" }, 173 | "runtime": { "name": "OCaml", "version": "4.13.1" } 174 | } |}] 175 | ;; 176 | 177 | let user = 178 | Metadata.User.make ~username:"admin" ~id:"000" ~email:"example@example.com" () 179 | ;; 180 | 181 | let%expect_test "metadata - user" = 182 | print_json (Metadata.User.yojson_of_t user); 183 | [%expect 184 | {| { "username": "admin", "id": "000", "email": "example@example.com" } |}]; 185 | (* An empty user is technically allowed and also not very useful *) 186 | let user = Metadata.User.make () in 187 | print_json (Metadata.User.yojson_of_t user); 188 | [%expect {| null |}] 189 | ;; 190 | 191 | let metadata = 192 | Metadata.make ~process ~system ~cloud ~user 193 | (Metadata.Service.make "testservice") 194 | ;; 195 | 196 | let%expect_test "metadata" = 197 | print_json (Metadata.yojson_of_t metadata); 198 | [%expect 199 | {| 200 | { 201 | "cloud": { 202 | "provider": "name", 203 | "region": "reg", 204 | "availability_zone": "az", 205 | "instance": { "id": "012", "name": "abc" }, 206 | "machine": { "type": "fast" }, 207 | "account": { "id": "012", "name": "abc" }, 208 | "project": { "id": "012", "name": "abc" } 209 | }, 210 | "process": { 211 | "pid": 2, 212 | "title": "process.exe", 213 | "ppid": 1, 214 | "argv": [ "hello", "world" ] 215 | }, 216 | "service": { 217 | "name": "testservice", 218 | "agent": { "name": "OCaml", "version": "n/a" }, 219 | "language": { "name": "OCaml", "version": "4.13.1" }, 220 | "runtime": { "name": "OCaml", "version": "4.13.1" } 221 | }, 222 | "system": { 223 | "architecture": "256bit", 224 | "hostname": "testhost", 225 | "platform": "testplatform", 226 | "container": { "id": "hiimacontainer" } 227 | }, 228 | "user": { 229 | "username": "admin", 230 | "id": "000", 231 | "email": "example@example.com" 232 | } 233 | } |}] 234 | ;; 235 | 236 | let span = 237 | Span.make 238 | ~duration:(Duration.of_span Mtime.Span.one) 239 | ~id:(Id.Span_id.create_gen state) 240 | ~kind:"test" 241 | ~transaction_id:(Id.Span_id.create_gen state) 242 | ~parent_id:(Id.Span_id.create_gen state) 243 | ~trace_id:(Id.Trace_id.create_gen state) 244 | ~timestamp:(Timestamp.of_us_since_epoch 123) 245 | "testspan" 246 | ;; 247 | 248 | let%expect_test "span" = 249 | print_json (Span.yojson_of_t span); 250 | [%expect 251 | {| 252 | { 253 | "duration": 1e-06, 254 | "id": "e5bef682a829d9c1", 255 | "name": "testspan", 256 | "transaction_id": "b03d453ece40e404", 257 | "parent_id": "1769c499c60d46a0", 258 | "trace_id": "20ba51f22b32eb39321acd340ce87f80", 259 | "type": "test", 260 | "timestamp": 123 261 | } |}] 262 | ;; 263 | 264 | let%expect_test "system info - platform" = 265 | let platform : System_info.Platform.t = 266 | { architecture = "fast"; hostname = "localhost"; platform = "awesome" } 267 | in 268 | print_json (System_info.Platform.yojson_of_t platform); 269 | [%expect 270 | {| { "architecture": "fast", "hostname": "localhost", "platform": "awesome" } |}] 271 | ;; 272 | 273 | let%expect_test "timestamp" = 274 | let timestamp = Timestamp.of_us_since_epoch 1234567890 in 275 | print_json (Timestamp.yojson_of_t timestamp); 276 | [%expect {| 1234567890 |}] 277 | ;; 278 | 279 | let transaction = 280 | Transaction.make 281 | ~request: 282 | (Context.Http.Request.make 283 | ~headers:[ ("foo", "bar"); ("test", "value") ] 284 | ~http_version:"HTTP/1.1" ~meth:"GET" (Uri.of_string "/hello") 285 | ) 286 | ~timestamp:(Timestamp.of_us_since_epoch (365 * 50 * 86_4000 * 1_000_000)) 287 | ~duration:(Duration.of_span Mtime.Span.one) 288 | ~id:(Id.Span_id.create_gen state) 289 | ~span_count:(Transaction.Span_count.make 12) 290 | ~trace_id:(Id.Trace_id.create_gen state) 291 | ~kind:"request" "test" 292 | ;; 293 | 294 | let%expect_test "transaction" = 295 | print_json (Transaction.yojson_of_t transaction); 296 | [%expect 297 | {| 298 | { 299 | "timestamp": 15768000000000000, 300 | "duration": 1e-06, 301 | "id": "a8d16b0a1559dc02", 302 | "span_count": { "started": 12 }, 303 | "trace_id": "b77ebdf068cb10014b841a2a47df3011", 304 | "type": "request", 305 | "name": "test", 306 | "context": { 307 | "request": { 308 | "headers": { "foo": "bar", "test": "value" }, 309 | "http_version": "HTTP/1.1", 310 | "method": "GET", 311 | "url": { "full": "/hello", "pathname": "/hello" } 312 | } 313 | } 314 | } |}]; 315 | let transaction = 316 | Transaction.make 317 | ~timestamp:(Timestamp.of_us_since_epoch (365 * 50 * 86_4000 * 1_000_000)) 318 | ~duration:(Duration.of_span Mtime.Span.one) 319 | ~id:(Id.Span_id.create_gen state) 320 | ~span_count:(Transaction.Span_count.make ~dropped:5 12) 321 | ~trace_id:(Id.Trace_id.create_gen state) 322 | ~kind:"request" "test" 323 | in 324 | print_json (Transaction.yojson_of_t transaction); 325 | [%expect 326 | {| 327 | { 328 | "timestamp": 15768000000000000, 329 | "duration": 1e-06, 330 | "id": "4e3d3c0df5a1f610", 331 | "span_count": { "dropped": 5, "started": 12 }, 332 | "trace_id": "11d0fb00078f8c303deab2a1651e57fc", 333 | "type": "request", 334 | "name": "test", 335 | "context": {} 336 | } |}] 337 | ;; 338 | 339 | let%expect_test "serialize request payloads" = 340 | print_json (Request.yojson_of_t (Request.Span span)); 341 | [%expect 342 | {| 343 | { 344 | "span": { 345 | "duration": 1e-06, 346 | "id": "e5bef682a829d9c1", 347 | "name": "testspan", 348 | "transaction_id": "b03d453ece40e404", 349 | "parent_id": "1769c499c60d46a0", 350 | "trace_id": "20ba51f22b32eb39321acd340ce87f80", 351 | "type": "test", 352 | "timestamp": 123 353 | } 354 | } |}]; 355 | print_json (Request.yojson_of_t (Request.Transaction transaction)); 356 | [%expect 357 | {| 358 | { 359 | "transaction": { 360 | "timestamp": 15768000000000000, 361 | "duration": 1e-06, 362 | "id": "a8d16b0a1559dc02", 363 | "span_count": { "started": 12 }, 364 | "trace_id": "b77ebdf068cb10014b841a2a47df3011", 365 | "type": "request", 366 | "name": "test", 367 | "context": { 368 | "request": { 369 | "headers": { "foo": "bar", "test": "value" }, 370 | "http_version": "HTTP/1.1", 371 | "method": "GET", 372 | "url": { "full": "/hello", "pathname": "/hello" } 373 | } 374 | } 375 | } 376 | } |}]; 377 | print_json (Request.yojson_of_t (Request.Metadata metadata)); 378 | [%expect 379 | {| 380 | { 381 | "metadata": { 382 | "cloud": { 383 | "provider": "name", 384 | "region": "reg", 385 | "availability_zone": "az", 386 | "instance": { "id": "012", "name": "abc" }, 387 | "machine": { "type": "fast" }, 388 | "account": { "id": "012", "name": "abc" }, 389 | "project": { "id": "012", "name": "abc" } 390 | }, 391 | "process": { 392 | "pid": 2, 393 | "title": "process.exe", 394 | "ppid": 1, 395 | "argv": [ "hello", "world" ] 396 | }, 397 | "service": { 398 | "name": "testservice", 399 | "agent": { "name": "OCaml", "version": "n/a" }, 400 | "language": { "name": "OCaml", "version": "4.13.1" }, 401 | "runtime": { "name": "OCaml", "version": "4.13.1" } 402 | }, 403 | "system": { 404 | "architecture": "256bit", 405 | "hostname": "testhost", 406 | "platform": "testplatform", 407 | "container": { "id": "hiimacontainer" } 408 | }, 409 | "user": { 410 | "username": "admin", 411 | "id": "000", 412 | "email": "example@example.com" 413 | } 414 | } 415 | } |}]; 416 | try test_error () with 417 | | exn -> 418 | let backtrace = Printexc.get_raw_backtrace () in 419 | let err = 420 | Error.make ~random_state:state ~backtrace ~exn 421 | ~timestamp:(Timestamp.of_us_since_epoch 123) 422 | () 423 | in 424 | print_json (Request.yojson_of_t (Request.Error err)); 425 | [%expect 426 | {| 427 | { 428 | "error": { 429 | "id": "a884f8dd451e53e894be98608fc09892", 430 | "timestamp": 123, 431 | "exception": { 432 | "message": "Apm_agent_tests.Json_serialization.Dummy_exn(\"Hello\")", 433 | "type": "exn", 434 | "stacktrace": [ 435 | { 436 | "filename": "test/json_serialization.ml", 437 | "lineno": 5, 438 | "function": "Apm_agent_tests__Json_serialization.boom", 439 | "colno": 14 440 | }, 441 | { 442 | "filename": "test/json_serialization.ml", 443 | "lineno": 7, 444 | "function": "Apm_agent_tests__Json_serialization.foo", 445 | "colno": 13 446 | }, 447 | { 448 | "filename": "test/json_serialization.ml", 449 | "lineno": 9, 450 | "function": "Apm_agent_tests__Json_serialization.test_error", 451 | "colno": 20 452 | }, 453 | { 454 | "filename": "test/json_serialization.ml", 455 | "lineno": 416, 456 | "function": "Apm_agent_tests__Json_serialization.(fun)", 457 | "colno": 6 458 | } 459 | ] 460 | } 461 | } 462 | } |}] 463 | ;; 464 | 465 | let%expect_test "metricsets" = 466 | let timestamp = Timestamp.of_us_since_epoch 123 in 467 | let samples = 468 | [ ("sample1", Metrics.Metric.Counter { value = 351.; unit_ = Some "ms" }) ] 469 | in 470 | let metrics = Metrics.create ~timestamp ~samples () in 471 | print_json (Request.yojson_of_t (Metrics metrics)); 472 | [%expect 473 | {| 474 | { 475 | "metricset": { 476 | "timestamp": 123, 477 | "labels": {}, 478 | "samples": { 479 | "sample1": { "type": "counter", "unit": "ms", "value": 351.0 } 480 | } 481 | } 482 | } |}]; 483 | let samples = 484 | [ 485 | ("sample1", Metrics.Metric.Counter { value = 351.; unit_ = Some "ms" }); 486 | ( "sample2", 487 | Metrics.Metric.Histogram 488 | { values = [ 0.1; 0.5; 0.8 ]; counts = [ 456L; 789L; 1241L ] } 489 | ); 490 | ] 491 | in 492 | let metrics = 493 | Metrics.create 494 | ~metric_span:(Metrics.Metric_span.make ~type_:"db" ~subtype:"insert") 495 | ~labels:[ ("foo", "bar"); ("hello", "world") ] 496 | ~timestamp ~samples () 497 | in 498 | print_json (Request.yojson_of_t (Metrics metrics)); 499 | [%expect 500 | {| 501 | { 502 | "metricset": { 503 | "timestamp": 123, 504 | "labels": { "foo": "bar", "hello": "world" }, 505 | "samples": { 506 | "sample1": { "type": "counter", "unit": "ms", "value": 351.0 }, 507 | "sample2": { 508 | "type": "histogram", 509 | "values": [ 0.1, 0.5, 0.8 ], 510 | "counts": [ 456, 789, 1241 ] 511 | } 512 | }, 513 | "span": { "type": "db", "subtype": "insert" } 514 | } 515 | } |}] 516 | ;; 517 | --------------------------------------------------------------------------------