commit 14a0fa922d65ba82702fbb24b64cd9f49d517d94 Author: Simon Cruanes Date: Wed Mar 16 15:58:08 2022 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5ee4596b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +_build +_opam +*.json +*.gz +*.db +.merlin + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..6a111cdb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/opentelemetry-proto"] + path = vendor/opentelemetry-proto + url = https://github.com/open-telemetry/opentelemetry-proto diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..2f7d7d9e --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ + +OPTS=--profile=release +all: + @dune build @all $(OPTS) + +test: + @dune runtest --force $(OPTS) + +clean: + @dune clean + +WATCH ?= @all +watch: + @dune build $(WATCH) -w $(OPTS) diff --git a/README.md b/README.md new file mode 100644 index 00000000..0acfce62 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ + +# Opentelemetry + +This project provides an API for instrumenting server software +using [opentelemetry](https://opentelemetry.io/docs), as well as +connectors to talk to opentelemetry software such as [jaeger](https://www.jaegertracing.io/). + +## Use + +- [ ] TODO: basic traces +- [ ] TODO: interface with `logs` (carry context around) +- [ ] TODO: interface with `lwt` + +## License + +MIT + +## Semantic Conventions + +Not supported yet. + +- [ ] [metrics](https://opentelemetry.io/docs/reference/specification/metrics/semantic_conventions/) +- [ ] [traces](https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/) +- [ ] [resources](https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/) diff --git a/dune b/dune new file mode 100644 index 00000000..f66632f8 --- /dev/null +++ b/dune @@ -0,0 +1,3 @@ + +(env + (_ (flags :standard -warn-error -a+8))) diff --git a/dune-project b/dune-project new file mode 100644 index 00000000..1a906de9 --- /dev/null +++ b/dune-project @@ -0,0 +1,32 @@ +(lang dune 2.3) +(name opentelemetry) +(generate_opam_files true) +(source + (github aestheticintegration/ocaml-opentelemetry)) + +(authors "the Imandra team") +(maintainers "the Imandra team") +(license MIT) +;(documentation https://url/to/documentation) + +(package + (name opentelemetry) + (synopsis "Instrumentation for https://opentelemetry.io") + (depends + (ocaml (>= "4.08")) + (dune (>= "2.3")) + (ocaml-protoc (>= 2.1))) + (depopts + ptime) + (tags + (instrumentation tracing opentelemetry datadog jaeger))) + +(package + (name opentelemetry-client-ocurl) + (depends + (ocaml (>= "4.08")) + (dune (>= "2.3")) + (opentelemetry (= :version)) + (ocaml-protoc (>= 2.1)) + ocurl) + (synopsis "Collector client for opentelemetry, using http + ocurl")) diff --git a/opentelemetry-client-ocurl.opam b/opentelemetry-client-ocurl.opam new file mode 100644 index 00000000..7ac94d64 --- /dev/null +++ b/opentelemetry-client-ocurl.opam @@ -0,0 +1,32 @@ +# This file is generated by dune, edit dune-project instead +opam-version: "2.0" +synopsis: "Collector client for opentelemetry, using http + ocurl" +maintainer: ["the Imandra team"] +authors: ["the Imandra team"] +license: "MIT" +homepage: "https://github.com/aestheticintegration/ocaml-opentelemetry" +bug-reports: + "https://github.com/aestheticintegration/ocaml-opentelemetry/issues" +depends: [ + "ocaml" {>= "4.08"} + "dune" {>= "2.3"} + "opentelemetry" {= version} + "ocaml-protoc" {>= "2.1"} + "ocurl" +] +build: [ + ["dune" "subst"] {pinned} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] +] +dev-repo: + "git+https://github.com/aestheticintegration/ocaml-opentelemetry.git" diff --git a/opentelemetry.opam b/opentelemetry.opam new file mode 100644 index 00000000..51eb11dc --- /dev/null +++ b/opentelemetry.opam @@ -0,0 +1,32 @@ +# This file is generated by dune, edit dune-project instead +opam-version: "2.0" +synopsis: "Instrumentation for https://opentelemetry.io" +maintainer: ["the Imandra team"] +authors: ["the Imandra team"] +license: "MIT" +tags: ["instrumentation" "tracing" "opentelemetry" "datadog" "jaeger"] +homepage: "https://github.com/aestheticintegration/ocaml-opentelemetry" +bug-reports: + "https://github.com/aestheticintegration/ocaml-opentelemetry/issues" +depends: [ + "ocaml" {>= "4.08"} + "dune" {>= "2.3"} + "ocaml-protoc" {>= "2.1"} +] +depopts: ["ptime"] +build: [ + ["dune" "subst"] {pinned} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] +] +dev-repo: + "git+https://github.com/aestheticintegration/ocaml-opentelemetry.git" diff --git a/src/client/dune b/src/client/dune new file mode 100644 index 00000000..c4f4685d --- /dev/null +++ b/src/client/dune @@ -0,0 +1,6 @@ + +(library + (name opentelemetry_client_ocurl) + (public_name opentelemetry-client-ocurl) + (libraries opentelemetry curl ocaml-protoc)) + diff --git a/src/client/opentelemetry_client_ocurl.ml b/src/client/opentelemetry_client_ocurl.ml new file mode 100644 index 00000000..1c35902a --- /dev/null +++ b/src/client/opentelemetry_client_ocurl.ml @@ -0,0 +1,118 @@ + +(* + https://github.com/open-telemetry/oteps/blob/main/text/0035-opentelemetry-protocol.md + https://github.com/open-telemetry/oteps/blob/main/text/0099-otlp-http.md + *) + +(* TODO *) + +open Opentelemetry + +let[@inline] (let@) f x = f x + +let default_url = "http://localhost:4318" +let url = ref (try Sys.getenv "OTEL_EXPORTER_OTLP_ENDPOINT" with _ -> default_url) +let set_url s = url := s + +let lock_ : (unit -> unit) ref = ref ignore +let unlock_ : (unit -> unit) ref = ref ignore +let set_mutex ~lock ~unlock : unit = + lock_ := lock; + unlock_ := unlock + +let[@inline] with_lock_ f = + !lock_(); + Fun.protect ~finally:!unlock_ f + +let _init = lazy ( + Curl.global_init Curl.CURLINIT_GLOBALALL; + at_exit Curl.global_cleanup; +) + +module Backend() : Opentelemetry.Collector.BACKEND = struct + let() = Lazy.force _init + + (* TODO: use Curl.Multi, etc. *) + + let encoder = Pbrt.Encoder.create() + let buf_res = Buffer.create 256 + + (* http client *) + let curl : Curl.t = Curl.init () + + let cleanup () = Curl.cleanup curl + + open Opentelemetry.Collector + + (* send the content to the remote endpoint/path *) + let send_ ~path ~decode (bod:string) : ('a, int * Status.status) result = + Curl.reset curl; + Curl.set_url curl (!url ^ path); + Curl.set_httppost curl []; + Curl.set_httpheader curl ["content-type: application/x-protobuf"]; + (* write body *) + Curl.set_readfunction curl + begin + let i = ref 0 in + (fun n -> + let len = min n (String.length bod - !i) in + String.sub bod !i len) + end; + Buffer.clear buf_res; + Curl.set_writefunction curl + (fun s -> Buffer.add_string buf_res s; String.length s); + match Curl.perform curl with + | () -> + let code = Curl.get_responsecode curl in + let dec = Pbrt.Decoder.of_string (Buffer.contents buf_res) in + if code >= 200 && code < 300 then ( + let res = decode dec in + Ok res + ) else ( + let status = Status.decode_status dec in + Error (code, status) + ) + | exception Curl.CurlException (_, code, msg) -> + let status = Status.default_status + ~code:(Int32.of_int code) ~message:(Bytes.unsafe_of_string msg) () in + Error(code, status) + + let report_err_ code status = + Format.eprintf "@[<2>opentelemetry: export failed with@ http code=%d@ status %a@]@." + code Status.pp_status status + + let send_trace (tr:Trace_service.export_trace_service_request) : unit = + let@() = with_lock_ in + Pbrt.Encoder.reset encoder; + Trace_service.encode_export_trace_service_request tr encoder; + match + send_ ~path:"/v1/traces" ~decode:(fun _ -> ()) + (Pbrt.Encoder.to_string encoder) + with + | Ok () -> () + | Error (code, status) -> report_err_ code status + + let send_metrics (m:Metrics_service.export_metrics_service_request) : unit = + let@() = with_lock_ in + Pbrt.Encoder.reset encoder; + Metrics_service.encode_export_metrics_service_request m encoder; + match + send_ ~path:"/v1/metrics" ~decode:(fun _ -> ()) + (Pbrt.Encoder.to_string encoder); + with + | Ok () -> () + | Error (code, status) -> report_err_ code status +end + +let setup_ () = + let module B = Backend() in + Opentelemetry.Collector.backend := Some (module B); + B.cleanup + +let setup() = + let cleanup = setup_() in + at_exit cleanup + +let with_setup f = + let cleanup = setup_() in + Fun.protect ~finally:cleanup f diff --git a/src/client/opentelemetry_client_ocurl.mli b/src/client/opentelemetry_client_ocurl.mli new file mode 100644 index 00000000..0790afef --- /dev/null +++ b/src/client/opentelemetry_client_ocurl.mli @@ -0,0 +1,16 @@ + +(* + TODO: more options from + https://opentelemetry.io/docs/reference/specification/protocol/exporter/ + *) + +val set_url : string -> unit +(** Url of the endpoint. Default is "http://localhost:4318", + or "OTEL_EXPORTER_OTLP_ENDPOINT" if set. *) + +val set_mutex : lock:(unit -> unit) -> unlock:(unit -> unit) -> unit + +val setup : unit -> unit +(** Setup endpoint. This modifies {!Opentelemetry.Collector.backend}. *) + +val with_setup : (unit -> 'a) -> 'a diff --git a/src/dune b/src/dune new file mode 100644 index 00000000..1de39362 --- /dev/null +++ b/src/dune @@ -0,0 +1,78 @@ +(library + (name opentelemetry) + (synopsis "API for opentelemetry instrumentation") + (flags :standard -warn-error -a+8) + (libraries + (select timestamp_clock.ml from + (ptime ptime.clock.os -> timestamp_clock.ptime.ml) + (unix -> timestamp_clock.unix.ml)) + ocaml-protoc) + (public_name opentelemetry)) + +; ### protobuf rules ### + +(rule + (targets status_types.ml status_types.mli + status_pb.ml status_pb.mli + status_pp.ml status_pp.mli) + (deps (:file status.proto)) + (action (run ocaml-protoc %{file} -ml_out . -pp -binary))) + +(rule + (targets common_types.ml common_types.mli + common_pb.ml common_pb.mli + common_pp.ml common_pp.mli) + (deps + (:file %{project_root}/vendor/opentelemetry-proto/opentelemetry/proto/common/v1/common.proto)) + (action (run ocaml-protoc %{file} + -I %{project_root}/vendor/opentelemetry-proto/ + -ml_out . -pp -binary))) + +(rule + (targets resource_types.ml resource_types.mli + resource_pb.ml resource_pb.mli + resource_pp.ml resource_pp.mli) + (deps + (:file %{project_root}/vendor/opentelemetry-proto/opentelemetry/proto/resource/v1/resource.proto)) + (action (run ocaml-protoc %{file} + -I %{project_root}/vendor/opentelemetry-proto/ + -ml_out . -pp -binary))) + +(rule + (targets trace_types.ml trace_types.mli + trace_pb.ml trace_pb.mli + trace_pp.ml trace_pp.mli) + (deps + (:file %{project_root}/vendor/opentelemetry-proto/opentelemetry/proto/trace/v1/trace.proto)) + (action (run ocaml-protoc %{file} + -I %{project_root}/vendor/opentelemetry-proto/ + -ml_out . -pp -binary))) + +(rule + (targets metrics_types.ml metrics_types.mli + metrics_pb.ml metrics_pb.mli + metrics_pp.ml metrics_pp.mli) + (deps + (:file %{project_root}/vendor/opentelemetry-proto/opentelemetry/proto/metrics/v1/metrics.proto)) + (action (run ocaml-protoc %{file} + -I %{project_root}/vendor/opentelemetry-proto/ + -ml_out . -pp -binary))) + +(rule + (targets metrics_service_types.ml + metrics_service_pp.ml metrics_service_pp.mli + metrics_service_pb.ml metrics_service_pb.mli) + (deps (:file %{project_root}/vendor/opentelemetry-proto/opentelemetry/proto/collector/metrics/v1/metrics_service.proto)) + (action (run ocaml-protoc %{file} + -I %{project_root}/vendor/opentelemetry-proto/ + -ml_out . -pp -binary))) + +(rule + (targets trace_service_types.ml + trace_service_pp.ml trace_service_pp.mli + trace_service_pb.ml trace_service_pb.mli) + (deps + (:file %{project_root}/vendor/opentelemetry-proto/opentelemetry/proto/collector/trace/v1/trace_service.proto)) + (action (run ocaml-protoc %{file} + -I %{project_root}/vendor/opentelemetry-proto/ + -ml_out . -pp -binary))) diff --git a/src/opentelemetry.ml b/src/opentelemetry.ml new file mode 100644 index 00000000..62f023b8 --- /dev/null +++ b/src/opentelemetry.ml @@ -0,0 +1,79 @@ + +(** Traces. + + See {{: https://opentelemetry.io/docs/reference/specification/overview/#tracing-signal} the spec} *) +module Trace = struct + include Trace_types + include Trace_pp + include Trace_pb +end + +(** Metrics. + + See {{: https://opentelemetry.io/docs/reference/specification/overview/#metric-signal} the spec} *) +module Metrics = struct + include Metrics_types + include Metrics_pp + include Metrics_pb +end + +module Common = struct + include Common_types + include Common_pp + include Common_pb +end + + +module Resource = struct + include Resource_types + include Resource_pp + include Resource_pb +end + +(* +module Span = Span +module Timestamp = Timestamp + *) + +(** Collector types + + These types are used by backend implementations, to send events to + collectors such as Jaeger. + + Note: most users will not need to touch this module *) +module Collector = struct + + module Trace_service = struct + include Trace_service_types + include Trace_service_pb + include Trace_service_pp + end + + module Metrics_service = struct + include Metrics_service_types + include Metrics_service_pp + include Metrics_service_pb + end + + module Status = struct + include Status_types + include Status_pp + include Status_pb + end + + (** Collector client interface. *) + module type BACKEND = sig + + val send_trace : Trace_service.export_trace_service_request -> unit + + val send_metrics : Metrics_service.export_metrics_service_request -> unit + + val cleanup : unit -> unit + end + + type backend = (module BACKEND) + + let backend : backend option ref = ref None + +end + diff --git a/src/status.proto b/src/status.proto new file mode 100644 index 00000000..9812db51 --- /dev/null +++ b/src/status.proto @@ -0,0 +1,10 @@ + +syntax = "proto3"; + +// from https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status?utm_source=godoc#Status + +message Status { + int32 code = 1; + bytes message = 2; + repeated bytes details = 3; +} diff --git a/src/timestamp.ml b/src/timestamp.ml new file mode 100644 index 00000000..23131bf9 --- /dev/null +++ b/src/timestamp.ml @@ -0,0 +1,4 @@ + +type t = float (* UTC *) + +let now = Timestamp_clock.now diff --git a/src/timestamp.mli b/src/timestamp.mli new file mode 100644 index 00000000..6c6ddf73 --- /dev/null +++ b/src/timestamp.mli @@ -0,0 +1,5 @@ + +type t + +val now : unit -> t + diff --git a/src/timestamp_clock.mli b/src/timestamp_clock.mli new file mode 100644 index 00000000..86deb712 --- /dev/null +++ b/src/timestamp_clock.mli @@ -0,0 +1,3 @@ + +val now : unit -> float +(** unix time in seconds, GMT *) diff --git a/src/timestamp_clock.ptime.ml b/src/timestamp_clock.ptime.ml new file mode 100644 index 00000000..d4930cc7 --- /dev/null +++ b/src/timestamp_clock.ptime.ml @@ -0,0 +1 @@ +let now () = Ptime_clock.now () |> Ptime.to_float_s diff --git a/src/timestamp_clock.unix.ml b/src/timestamp_clock.unix.ml new file mode 100644 index 00000000..0b9e4d1b --- /dev/null +++ b/src/timestamp_clock.unix.ml @@ -0,0 +1,2 @@ + +let now () = Unix.gettimeofday() diff --git a/vendor/opentelemetry-proto b/vendor/opentelemetry-proto new file mode 160000 index 00000000..cc4ed55c --- /dev/null +++ b/vendor/opentelemetry-proto @@ -0,0 +1 @@ +Subproject commit cc4ed55c082cb75e084d40b4ddf3805eda099f97