{"id":49574662,"url":"https://github.com/benoitc/instrument","last_synced_at":"2026-05-03T16:05:30.470Z","repository":{"id":348132518,"uuid":"1196629877","full_name":"benoitc/instrument","owner":"benoitc","description":"Erlang metrics library with Prometheus export","archived":false,"fork":false,"pushed_at":"2026-05-03T14:33:36.000Z","size":609,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-03T15:28:46.815Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Erlang","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/benoitc.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-30T22:13:23.000Z","updated_at":"2026-05-03T14:31:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/benoitc/instrument","commit_stats":null,"previous_names":["benoitc/instrument"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/benoitc/instrument","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Finstrument","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Finstrument/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Finstrument/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Finstrument/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/benoitc","download_url":"https://codeload.github.com/benoitc/instrument/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/benoitc%2Finstrument/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32575164,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T06:36:36.687Z","status":"ssl_error","status_checked_at":"2026-05-03T06:36:09.306Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-03T16:05:29.672Z","updated_at":"2026-05-03T16:05:30.460Z","avatar_url":"https://github.com/benoitc.png","language":"Erlang","funding_links":[],"categories":[],"sub_categories":[],"readme":"# instrument\n\n[![Hex.pm](https://img.shields.io/hexpm/v/instrument.svg)](https://hex.pm/packages/instrument)\n[![Build Status](https://github.com/benoitc/instrument/workflows/CI/badge.svg)](https://github.com/benoitc/instrument/actions)\n\nOpenTelemetry-compatible observability library for Erlang with high-performance NIF-based metrics.\n\n## OpenTelemetry Compatibility\n\n| Component | Version | Notes |\n|-----------|---------|-------|\n| OTLP Protocol | 1.0+ | HTTP/JSON encoding for traces, metrics, logs |\n| Trace API | 1.0+ | Spans, attributes, events, links, status |\n| Metrics API | 1.0+ | Counter, Gauge, Histogram with attributes |\n| Context/Propagation | 1.0+ | W3C TraceContext, W3C Baggage, B3/B3Multi |\n| Resource | 1.0+ | Service name, SDK info, environment detection |\n| Sampling | 1.0+ | always_on, always_off, traceidratio, parentbased |\n\nTested with:\n- Jaeger 1.50+ (OTLP receiver)\n- Prometheus 2.40+ (scraping /metrics endpoint)\n\n## Features\n\n- **OpenTelemetry API**: Full OTel-compatible Meter and Tracer interfaces\n- **Distributed Tracing**: Spans with W3C TraceContext, B3, and Baggage propagation\n- **High-Performance Metrics**: NIF-based atomic counters, gauges, and histograms\n- **Labeled Metrics**: Vector metrics with dimension labels (attributes)\n- **Span Attributes**: Indexable metadata on spans for filtering and querying\n- **Export Options**: OTLP, Prometheus, Console exporters\n- **Test Framework**: Built-in collectors and assertions for testing instrumented code\n- **No External Dependencies**: Pure Erlang/OTP implementation\n\n## Installation\n\n### rebar3\n\n```erlang\n{deps, [\n    {instrument, \"1.0.0\"}\n]}.\n```\n\n### mix (Elixir)\n\n```elixir\n{:instrument, \"~\u003e 1.0.0\"}\n```\n\n## Quick Start\n\n### OpenTelemetry API (Recommended)\n\n```erlang\n%% Get a meter for your service\nMeter = instrument_meter:get_meter(\u003c\u003c\"my_service\"\u003e\u003e),\n\n%% Create instruments with attributes support\nCounter = instrument_meter:create_counter(Meter, \u003c\u003c\"http_requests_total\"\u003e\u003e, #{\n    description =\u003e \u003c\u003c\"Total HTTP requests\"\u003e\u003e,\n    unit =\u003e \u003c\u003c\"1\"\u003e\u003e\n}),\n\nHistogram = instrument_meter:create_histogram(Meter, \u003c\u003c\"http_request_duration_seconds\"\u003e\u003e, #{\n    description =\u003e \u003c\u003c\"Request duration\"\u003e\u003e,\n    unit =\u003e \u003c\u003c\"s\"\u003e\u003e\n}),\n\n%% Record with attributes (dimensions)\ninstrument_meter:add(Counter, 1, #{method =\u003e \u003c\u003c\"GET\"\u003e\u003e, status =\u003e 200}),\ninstrument_meter:record(Histogram, 0.125, #{endpoint =\u003e \u003c\u003c\"/api/users\"\u003e\u003e}).\n```\n\n### Distributed Tracing\n\n```erlang\n%% Create spans with indexable attributes\ninstrument_tracer:with_span(\u003c\u003c\"process_order\"\u003e\u003e, #{kind =\u003e server}, fun() -\u003e\n    %% Add attributes - these are indexed and queryable in backends\n    instrument_tracer:set_attributes(#{\n        \u003c\u003c\"order.id\"\u003e\u003e =\u003e \u003c\u003c\"12345\"\u003e\u003e,\n        \u003c\u003c\"customer.id\"\u003e\u003e =\u003e \u003c\u003c\"67890\"\u003e\u003e,\n        \u003c\u003c\"order.total\"\u003e\u003e =\u003e 99.99\n    }),\n\n    %% Add timestamped events\n    instrument_tracer:add_event(\u003c\u003c\"order_validated\"\u003e\u003e),\n\n    Result = do_work(),\n\n    instrument_tracer:set_status(ok),\n    Result\nend).\n```\n\n## Standalone Metrics (Without OTel)\n\nFor simple use cases, use metrics directly without the OTel API:\n\n### Counter\n\n```erlang\n%% Create and use a counter\nCounter = instrument_metric:new_counter(requests_total, \"Total requests\"),\ninstrument_metric:inc_counter(Counter),\ninstrument_metric:inc_counter(Counter, 5),\nValue = instrument_metric:get_counter(Counter).  %% 6.0\n```\n\n### Gauge\n\n```erlang\n%% Create and use a gauge\nGauge = instrument_metric:new_gauge(connections_active, \"Active connections\"),\ninstrument_metric:set_gauge(Gauge, 100),\ninstrument_metric:inc_gauge(Gauge),       %% 101\ninstrument_metric:dec_gauge(Gauge, 5),    %% 96\nValue = instrument_metric:get_gauge(Gauge).\n```\n\n### Histogram\n\n```erlang\n%% Create with default buckets\nHistogram = instrument_metric:new_histogram(request_duration_seconds, \"Request duration\"),\n\n%% Or with custom buckets\nHistogram2 = instrument_metric:new_histogram(response_size_bytes, \"Response size\",\n    [100, 500, 1000, 5000, 10000]),\n\n%% Record observations\ninstrument_metric:observe_histogram(Histogram, 0.125),\n\n%% Get distribution data\n#{count := Count, sum := Sum, buckets := Buckets} = instrument_metric:get_histogram(Histogram).\n```\n\n### Vector Metrics (Labeled)\n\nAdd dimensions to standalone metrics:\n\n```erlang\n%% Create vector metrics\ninstrument_metric:new_counter_vec(http_requests_total, \"HTTP requests\", [method, status]),\ninstrument_metric:new_gauge_vec(pool_connections, \"Pool connections\", [pool, state]),\ninstrument_metric:new_histogram_vec(db_query_duration, \"Query duration\", [operation]),\n\n%% Record with labels\ninstrument_metric:inc_counter_vec(http_requests_total, [\"GET\", \"200\"]),\ninstrument_metric:set_gauge_vec(pool_connections, [\"default\", \"active\"], 10),\ninstrument_metric:observe_histogram_vec(db_query_duration, [\"SELECT\"], 0.05).\n```\n\n## Context Propagation\n\n### W3C TraceContext (Default)\n\n```erlang\n%% Inject into outgoing request headers\nHeaders = instrument_propagation:inject_headers(instrument_context:current()),\n\n%% Extract from incoming request headers\nCtx = instrument_propagation:extract_headers(IncomingHeaders),\ninstrument_context:attach(Ctx).\n```\n\n### B3 Propagation (Zipkin)\n\nConfigure B3 propagation for Zipkin compatibility:\n\n```erlang\n%% Via environment variable\nos:putenv(\"OTEL_PROPAGATORS\", \"b3\"),\ninstrument_config:init().\n\n%% Or programmatically\ninstrument_propagator:set_propagators([instrument_propagator_b3]).\n```\n\nB3 multi-header format:\n\n```erlang\nos:putenv(\"OTEL_PROPAGATORS\", \"b3multi\"),\ninstrument_config:init().\n```\n\n### Cross-Process Propagation\n\n```erlang\n%% Spawn with trace context preserved\ninstrument_propagation:spawn(fun() -\u003e\n    instrument_tracer:with_span(\u003c\u003c\"background_task\"\u003e\u003e, fun() -\u003e\n        do_work()\n    end)\nend).\n```\n\n## Span Attributes\n\nAttributes are key-value pairs attached to spans. They are indexed by observability backends, enabling filtering, grouping, and querying.\n\n```erlang\ninstrument_tracer:with_span(\u003c\u003c\"http_request\"\u003e\u003e, #{kind =\u003e server}, fun() -\u003e\n    %% Set attributes for indexing and querying\n    instrument_tracer:set_attributes(#{\n        %% HTTP semantic conventions\n        \u003c\u003c\"http.method\"\u003e\u003e =\u003e \u003c\u003c\"POST\"\u003e\u003e,\n        \u003c\u003c\"http.url\"\u003e\u003e =\u003e \u003c\u003c\"https://api.example.com/orders\"\u003e\u003e,\n        \u003c\u003c\"http.status_code\"\u003e\u003e =\u003e 201,\n\n        %% Custom business attributes\n        \u003c\u003c\"order.id\"\u003e\u003e =\u003e OrderId,\n        \u003c\u003c\"customer.tier\"\u003e\u003e =\u003e \u003c\u003c\"premium\"\u003e\u003e,\n        \u003c\u003c\"order.item_count\"\u003e\u003e =\u003e length(Items)\n    }),\n\n    %% These attributes can be used in your backend to:\n    %% - Filter traces by customer tier\n    %% - Group latencies by HTTP method\n    %% - Alert on specific order patterns\n    process_order(Order)\nend).\n```\n\n## Prometheus Export\n\n```erlang\n%% Get Prometheus-formatted metrics\nBody = instrument_prometheus:format(),\nContentType = instrument_prometheus:content_type().\n\n%% In your HTTP handler\nhandle_metrics(_Req) -\u003e\n    {200, [{\u003c\u003c\"content-type\"\u003e\u003e, ContentType}], Body}.\n```\n\n## Logger Integration\n\n```erlang\n%% Install at application start\ninstrument_logger:install(),\n\n%% Logs within spans include trace_id and span_id\ninstrument_tracer:with_span(\u003c\u003c\"my_operation\"\u003e\u003e, fun() -\u003e\n    logger:info(\"Processing request\"),  %% Includes trace context\n    do_work()\nend).\n```\n\n## Testing Instrumentation\n\nThe `instrument_test` module provides collectors and assertions for testing instrumented code:\n\n```erlang\n-module(my_module_test).\n-include_lib(\"eunit/include/eunit.hrl\").\n\nmy_test_() -\u003e\n    {setup,\n        fun() -\u003e instrument_test:setup() end,\n        fun(_) -\u003e instrument_test:cleanup() end,\n        [fun test_span_creation/0]\n    }.\n\ntest_span_creation() -\u003e\n    instrument_test:reset(),\n\n    %% Call instrumented code\n    my_module:process_request(#{id =\u003e 123}),\n\n    %% Assert span was created with correct attributes\n    instrument_test:assert_span_exists(\u003c\u003c\"process_request\"\u003e\u003e),\n    instrument_test:assert_span_attribute(\u003c\u003c\"process_request\"\u003e\u003e, \u003c\u003c\"request.id\"\u003e\u003e, 123),\n    instrument_test:assert_span_status(\u003c\u003c\"process_request\"\u003e\u003e, ok).\n```\n\nTest metrics and logs:\n\n```erlang\n%% Assert counter value\ninstrument_test:assert_counter(requests_total, 5.0),\n\n%% Assert gauge value\ninstrument_test:assert_gauge(active_connections, 42.0),\n\n%% Assert log exists with trace context\ninstrument_test:assert_log_exists(\u003c\u003c\"Processing request\"\u003e\u003e),\ninstrument_test:assert_log_trace_context(\u003c\u003c\"Processing request\"\u003e\u003e).\n```\n\nSee the [Testing Instrumentation Guide](guides/testing_instrumentation.md) for details.\n\n## Documentation\n\n### Erlang Observability Handbook\n\nA step-by-step guide to instrumenting Erlang applications:\n\n- [Introduction](book/00_introduction.md)\n- [Why Observability Matters](book/01_why_observability.md)\n- [Your First Metrics](book/02_first_metrics.md)\n- [Adding Dimensions with Labels](book/03_labels.md)\n- [Understanding Traces](book/04_understanding_traces.md)\n- [Building Effective Spans](book/05_effective_spans.md)\n- [Connecting Services](book/06_connecting_services.md)\n- [Logs That Tell the Story](book/07_logs.md)\n- [Getting Data Out](book/08_exporters.md)\n- [Sampling for Scale](book/09_sampling.md)\n- [Complete Example](book/10_complete_example.md)\n- [Quick Reference](book/appendix_a_quick_reference.md)\n- [Troubleshooting](book/appendix_b_troubleshooting.md)\n\n### Guides\n\n- [Getting Started Guide](guides/getting_started.md)\n- [Elixir Users Guide](guides/elixir_guide.md)\n- [Instrumentation Guide](guides/instrumentation_guide.md)\n- [Context Propagation Guide](guides/context_propagation.md)\n- [Sampling and Processing Guide](guides/sampling_and_processing.md)\n- [Exporters Guide](guides/exporters.md)\n- [Features Reference](guides/features.md)\n- [Benchmarks](guides/benchmarks.md)\n\n### Reference\n\n- [Metrics Reference](guides/metrics_reference.md)\n- [Tracing Reference](guides/tracing_reference.md)\n- [Semantic Conventions](guides/semantic_conventions.md)\n- [Testing Instrumentation](guides/testing_instrumentation.md)\n- [Production Operations](guides/production_operations.md)\n- [Migration Guide](guides/migration.md)\n\n## Modules\n\n| Module | Purpose |\n|--------|---------|\n| `instrument` | Standalone metrics API (counter, gauge, histogram) |\n| `instrument_meter` | OpenTelemetry Meter API |\n| `instrument_tracer` | Span creation and tracing |\n| `instrument_context` | Context management |\n| `instrument_propagation` | Cross-process/service propagation |\n| `instrument_prometheus` | Prometheus export |\n| `instrument_test` | Test collectors and assertions |\n\n## Configuration\n\n### Environment Variables\n\n| Variable | Description |\n|----------|-------------|\n| `OTEL_SERVICE_NAME` | Service name for resource |\n| `OTEL_TRACES_SAMPLER` | Sampler type (always_on, always_off, traceidratio, parentbased_*) |\n| `OTEL_TRACES_SAMPLER_ARG` | Sampler argument (e.g., probability ratio) |\n| `OTEL_PROPAGATORS` | Propagators (tracecontext, baggage, b3, b3multi) |\n| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL |\n\n### Application Config\n\n```erlang\n%% In sys.config\n{instrument, [\n    {service_name, \u003c\u003c\"my_service\"\u003e\u003e},\n    {sampler, {instrument_sampler_probability, #{ratio =\u003e 0.1}}}\n]}.\n```\n\n## Building\n\n```bash\nrebar3 compile\nrebar3 ct\nrebar3 dialyzer\n```\n\n## License\n\nMIT License - see [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbenoitc%2Finstrument","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbenoitc%2Finstrument","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbenoitc%2Finstrument/lists"}